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,5 @@
1
+ module ActiveJob
2
+ module Errors
3
+ class InvalidOperation < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ module ActiveJob
2
+ module Errors
3
+ class JobNotFoundError < StandardError
4
+ attr_reader :job_relation
5
+
6
+ def initialize(job_or_job_id, job_relation)
7
+ @job_relation = job_relation
8
+
9
+ job_id = job_or_job_id.is_a?(ActiveJob::Base) ? job_or_job_id.job_id : job_or_job_id
10
+ super "Job with id '#{job_id}' not found"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveJob
2
+ module Errors
3
+ class QueryError < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,40 @@
1
+ # TODO: These (or a version of them) should be moved to +ActiveJob::Core+
2
+ # and related concerns when upstreamed.
3
+ module ActiveJob::Executing
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ attr_accessor :raw_data, :position, :finished_at, :blocked_by, :blocked_until, :worker_id, :started_at, :status
8
+ attr_reader :serialized_arguments
9
+
10
+ thread_cattr_accessor :current_queue_adapter
11
+ end
12
+
13
+ class_methods do
14
+ def queue_adapter
15
+ ActiveJob::Base.current_queue_adapter || super
16
+ end
17
+ end
18
+
19
+ def retry
20
+ ActiveJob.jobs.failed.retry_job(self)
21
+ end
22
+
23
+ def discard
24
+ jobs_relation_for_discarding.discard_job(self)
25
+ end
26
+
27
+ def dispatch
28
+ ActiveJob.jobs.dispatch_job(self)
29
+ end
30
+
31
+ private
32
+ def jobs_relation_for_discarding
33
+ case status
34
+ when :failed then ActiveJob.jobs.failed
35
+ when :pending then ActiveJob.jobs.pending.where(queue_name: queue_name)
36
+ else
37
+ ActiveJob.jobs
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,8 @@
1
+ # Information about a given error when executing a job.
2
+ #
3
+ # It's attached to failed jobs at +ActiveJob::Base#last_execution_error+.
4
+ ActiveJob::ExecutionError = Struct.new(:error_class, :message, :backtrace, keyword_init: true) do
5
+ def to_s
6
+ "ERROR #{error_class}: #{message}\n#{backtrace&.collect { |line| "\t#{line}" }&.join("\n")}"
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module ActiveJob::Failed
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ attr_accessor :last_execution_error, :failed_at
6
+ end
7
+ end
@@ -0,0 +1,38 @@
1
+ # A proxy for managing jobs without having to load the corresponding
2
+ # job class.
3
+ #
4
+ # This is useful for managing jobs without having the job classes
5
+ # present in the code base.
6
+ class ActiveJob::JobProxy < ActiveJob::Base
7
+ class UnsupportedError < StandardError; end
8
+
9
+ attr_reader :job_class_name
10
+ # Raw data with the sensitive user data filtered out.
11
+ attr_accessor :filtered_raw_data
12
+
13
+ def initialize(job_data)
14
+ super
15
+ @job_class_name = job_data["job_class"]
16
+ deserialize(job_data)
17
+ end
18
+
19
+ def serialize
20
+ super.tap do |json|
21
+ json["job_class"] = @job_class_name
22
+ end
23
+ end
24
+
25
+ def perform_now
26
+ raise UnsupportedError, "A JobProxy doesn't support immediate execution, only enqueuing."
27
+ end
28
+
29
+ def duration
30
+ finished_at - (scheduled_at || enqueued_at)
31
+ end
32
+
33
+ ActiveJob::JobsRelation::STATUSES.each do |status|
34
+ define_method "#{status}?" do
35
+ self.status == status
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,318 @@
1
+ # A relation of jobs that can be filtered and acted on.
2
+ #
3
+ # Relations of jobs are normally fetched via +ActiveJob.jobs+
4
+ # or through a given queue (+ActiveJob::Queue#jobs+).
5
+ #
6
+ # This class offers a fluid interface to query a subset of jobs. For
7
+ # example:
8
+ #
9
+ # queue = ActiveJob.queues[:default]
10
+ # queue.jobs.limit(10).where(job_class_name: "DummyJob").last
11
+ #
12
+ # Relations are enumerable, so you can use +Enumerable+ methods on them.
13
+ # Notice however that using these methods will imply loading all the relation
14
+ # in memory, which could introduce performance concerns.
15
+ #
16
+ # Internally, +ActiveJob+ will always use paginated queries to the underlying
17
+ # queue adapter. The page size can be controlled via +config.active_job.default_page_size+
18
+ # (1000 by default).
19
+ #
20
+ # There are additional performance concerns depending on the configured
21
+ # adapter. Please check +ActiveJob::Relation#where+, +ActiveJob::Relation#count+.
22
+ class ActiveJob::JobsRelation
23
+ include Enumerable
24
+
25
+ STATUSES = %i[ pending failed in_progress blocked scheduled finished ]
26
+ FILTERS = %i[ queue_name job_class_name ]
27
+
28
+ PROPERTIES = %i[ queue_name status offset_value limit_value job_class_name worker_id recurring_task_id finished_at ]
29
+ attr_reader(*PROPERTIES, :default_page_size)
30
+
31
+ delegate :last, :[], :reverse, to: :to_a
32
+ delegate :logger, to: MissionControl::Jobs
33
+
34
+ ALL_JOBS_LIMIT = 100_000_000 # When no limit value it defaults to "all jobs"
35
+
36
+ def initialize(queue_adapter: ActiveJob::Base.queue_adapter, default_page_size: ActiveJob::Base.default_page_size)
37
+ @queue_adapter = queue_adapter
38
+ @default_page_size = default_page_size
39
+
40
+ set_defaults
41
+ end
42
+
43
+ # Returns a +ActiveJob::JobsRelation+ with the configured filtering options.
44
+ #
45
+ # === Options
46
+ #
47
+ # * <tt>:job_class_name</tt> - To only include the jobs of a given class.
48
+ # Depending on the configured queue adapter, this will perform the
49
+ # filtering in memory, which could introduce performance concerns
50
+ # for large sets of jobs.
51
+ # * <tt>:queue_name</tt> - To only include the jobs in the provided queue.
52
+ # * <tt>:worker_id</tt> - To only include the jobs processed by the provided worker.
53
+ # * <tt>:recurring_task_id</tt> - To only include the jobs corresponding to runs of a recurring task.
54
+ # * <tt>:finished_at</tt> - (Range) To only include the jobs finished between the provided range
55
+ def where(job_class_name: nil, queue_name: nil, worker_id: nil, recurring_task_id: nil, finished_at: nil)
56
+ # Remove nil arguments to avoid overriding parameters when concatenating +where+ clauses
57
+ arguments = { job_class_name: job_class_name,
58
+ queue_name: queue_name&.to_s,
59
+ worker_id: worker_id,
60
+ recurring_task_id: recurring_task_id,
61
+ finished_at: finished_at
62
+ }.compact
63
+
64
+ clone_with(**arguments)
65
+ end
66
+
67
+ def with_status(status)
68
+ if status.to_sym.in? STATUSES
69
+ clone_with status: status.to_sym
70
+ else
71
+ self
72
+ end
73
+ end
74
+
75
+ STATUSES.each do |status|
76
+ define_method status do
77
+ with_status(status)
78
+ end
79
+
80
+ define_method "#{status}?" do
81
+ self.status == status
82
+ end
83
+ end
84
+
85
+ # Sets an offset for the jobs-fetching query. The first position is 0.
86
+ def offset(offset)
87
+ clone_with offset_value: offset
88
+ end
89
+
90
+ # Sets the max number of jobs to fetch in the query.
91
+ def limit(limit)
92
+ clone_with limit_value: limit
93
+ end
94
+
95
+ # Returns the number of jobs in the relation.
96
+ #
97
+ # When filtering jobs, if the adapter doesn't support the filter(s)
98
+ # directly, this will load all the jobs in memory to filter them.
99
+ def count
100
+ if loaded? || filtering_needed?
101
+ to_a.length
102
+ else
103
+ query_count
104
+ end
105
+ end
106
+
107
+ alias length count
108
+ alias size count
109
+
110
+ def empty?
111
+ count == 0
112
+ end
113
+
114
+ def to_s
115
+ properties_with_values = PROPERTIES.collect do |name|
116
+ value = public_send(name)
117
+ "#{name}: #{value}" unless value.nil?
118
+ end.compact.join(", ")
119
+ "<Jobs with [#{properties_with_values}]> (loaded: #{loaded?})"
120
+ end
121
+
122
+ alias inspect to_s
123
+
124
+ def each(&block)
125
+ loaded_jobs&.each(&block) || load_jobs(&block)
126
+ end
127
+
128
+ # Retry all the jobs in the queue.
129
+ #
130
+ # This operation is only valid for sets of failed jobs. It will
131
+ # raise an error +ActiveJob::Errors::InvalidOperation+ otherwise.
132
+ def retry_all
133
+ ensure_failed_status
134
+ queue_adapter.retry_all_jobs(self)
135
+ nil
136
+ end
137
+
138
+ # Retry the provided job.
139
+ #
140
+ # This operation is only valid for sets of failed jobs. It will
141
+ # raise an error +ActiveJob::Errors::InvalidOperation+ otherwise.
142
+ def retry_job(job)
143
+ ensure_failed_status
144
+ queue_adapter.retry_job(job, self)
145
+ end
146
+
147
+ # Discard all the jobs in the relation.
148
+ def discard_all
149
+ queue_adapter.discard_all_jobs(self)
150
+ nil
151
+ end
152
+
153
+ # Discard the provided job.
154
+ def discard_job(job)
155
+ queue_adapter.discard_job(job, self)
156
+ end
157
+
158
+ # Dispatch the provided job.
159
+ #
160
+ # This operation is only valid for blocked or scheduled jobs. It will
161
+ # raise an error +ActiveJob::Errors::InvalidOperation+ otherwise.
162
+ def dispatch_job(job)
163
+ raise ActiveJob::Errors::InvalidOperation, "This operation can only be performed on blocked or scheduled jobs, but this job is #{job.status}" unless job.blocked? || job.scheduled?
164
+
165
+ queue_adapter.dispatch_job(job, self)
166
+ end
167
+
168
+ # Find a job by id.
169
+ #
170
+ # Returns nil when not found.
171
+ def find_by_id(job_id)
172
+ queue_adapter.find_job(job_id, self)
173
+ end
174
+
175
+ # Find a job by id.
176
+ #
177
+ # Raises +ActiveJob::Errors::JobNotFoundError+ when not found.
178
+ def find_by_id!(job_id)
179
+ queue_adapter.find_job(job_id, self) or raise ActiveJob::Errors::JobNotFoundError.new(job_id, self)
180
+ end
181
+
182
+ # Returns an array of jobs class names in the first +from_first+ jobs.
183
+ def job_class_names(from_first: 500)
184
+ first(from_first).collect(&:job_class_name).uniq
185
+ end
186
+
187
+ def reload
188
+ @count = nil
189
+ @loaded_jobs = nil
190
+ @filters = nil
191
+
192
+ self
193
+ end
194
+
195
+ def in_batches(of: default_page_size, order: :asc, &block)
196
+ validate_looping_in_batches_is_possible
197
+
198
+ case order
199
+ when :asc
200
+ in_ascending_batches(of: of, &block)
201
+ when :desc
202
+ in_descending_batches(of: of, &block)
203
+ else
204
+ raise "Unsupported order: #{order}. Valid values: :asc, :desc."
205
+ end
206
+ end
207
+
208
+ def paginated?
209
+ offset_value > 0 || limit_value_provided?
210
+ end
211
+
212
+ def limit_value_provided?
213
+ limit_value.present? && limit_value != ActiveJob::JobsRelation::ALL_JOBS_LIMIT
214
+ end
215
+
216
+ def filtering_needed?
217
+ filters.any?
218
+ end
219
+
220
+ private
221
+ attr_reader :queue_adapter, :loaded_jobs
222
+ attr_writer(*PROPERTIES)
223
+
224
+ def set_defaults
225
+ self.offset_value = 0
226
+ self.limit_value = ALL_JOBS_LIMIT
227
+ end
228
+
229
+ def clone_with(**properties)
230
+ dup.reload.tap do |relation|
231
+ properties.each do |key, value|
232
+ relation.send("#{key}=", value)
233
+ end
234
+ end
235
+ end
236
+
237
+ def query_count
238
+ @count ||= queue_adapter.jobs_count(self)
239
+ end
240
+
241
+ def load_jobs
242
+ @loaded_jobs = []
243
+ perform_each do |job|
244
+ @loaded_jobs << job
245
+ yield job
246
+ end
247
+ end
248
+
249
+ def perform_each
250
+ current_offset = offset_value
251
+ pending_count = limit_value || Float::INFINITY
252
+
253
+ begin
254
+ limit = [ pending_count, default_page_size ].min
255
+ page = offset(current_offset).limit(limit)
256
+ jobs = queue_adapter.fetch_jobs(page)
257
+ finished = jobs.empty?
258
+ jobs = filter(jobs) if filtering_needed?
259
+ Array(jobs).each { |job| yield job }
260
+ current_offset += limit
261
+ pending_count -= jobs.length
262
+ end until finished || pending_count.zero?
263
+ end
264
+
265
+ def loaded?
266
+ !@loaded_jobs.nil?
267
+ end
268
+
269
+ # Filtering for not natively supported filters is performed in memory
270
+ def filter(jobs)
271
+ jobs.filter { |job| satisfy_filter?(job) }
272
+ end
273
+
274
+ def satisfy_filter?(job)
275
+ filters.all? { |property| public_send(property) == job.public_send(property) }
276
+ end
277
+
278
+ def filters
279
+ @filters ||= FILTERS.select { |property| public_send(property).present? && !queue_adapter.supports_job_filter?(self, property) }
280
+ end
281
+
282
+ def ensure_failed_status
283
+ raise ActiveJob::Errors::InvalidOperation, "This operation can only be performed on failed jobs, but these jobs are #{status}" unless failed?
284
+ end
285
+
286
+ def validate_looping_in_batches_is_possible
287
+ raise ActiveJob::Errors::InvalidOperation, "Looping in batches is not compatible with providing offset or limit" if paginated?
288
+ end
289
+
290
+ def in_ascending_batches(of:)
291
+ current_offset = 0
292
+ max = count
293
+ begin
294
+ page = offset(current_offset).limit(of)
295
+ current_offset += of
296
+ logger.info page
297
+ yield page
298
+ wait_batch_delay
299
+ end until current_offset >= max
300
+ end
301
+
302
+ def in_descending_batches(of:)
303
+ current_offset = count - of
304
+
305
+ begin
306
+ limit = current_offset < 0 ? of + current_offset : of
307
+ page = offset([ current_offset, 0 ].max).limit(limit)
308
+ current_offset -= of
309
+ logger.info page
310
+ yield page
311
+ wait_batch_delay
312
+ end until current_offset + of <= 0
313
+ end
314
+
315
+ def wait_batch_delay
316
+ sleep MissionControl::Jobs.delay_between_bulk_operation_batches if MissionControl::Jobs.delay_between_bulk_operation_batches.to_i > 0
317
+ end
318
+ end
@@ -0,0 +1,21 @@
1
+ # Progress reporting mixed into Active Job. Unused methods are inert for jobs
2
+ # that never call +progress+.
3
+ module ActiveJob::ProgressReporting
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ discard_on MissionControl::Jobs::JobStoppedError
8
+
9
+ after_perform { |job| job.send(:complete_mission_control_progress) }
10
+ after_discard { |job, _error| job.send(:complete_mission_control_progress) }
11
+ end
12
+
13
+ def progress
14
+ @mission_control_progress_reporter ||= MissionControl::Jobs::Progress::Reporter.new(job_id)
15
+ end
16
+
17
+ private
18
+ def complete_mission_control_progress
19
+ @mission_control_progress_reporter&.complete
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+ module ActiveJob::Querying
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ # ActiveJob will use pagination internally when fetching relations of jobs. This
6
+ # parameter sets the max amount of jobs to fetch in each data store query.
7
+ class_attribute :default_page_size, default: 1000
8
+ end
9
+
10
+ class_methods do
11
+ # Returns the list of queues.
12
+ #
13
+ # See +ActiveJob::Queues+
14
+ def queues
15
+ ActiveJob::Queues.new(fetch_queues)
16
+ end
17
+
18
+ def jobs
19
+ ActiveJob::JobsRelation.new(queue_adapter: queue_adapter, default_page_size: default_page_size)
20
+ end
21
+
22
+ private
23
+ def fetch_queues
24
+ queue_adapter.queues.collect do |queue|
25
+ ActiveJob::Queue.new(queue[:name], size: queue[:size], active: queue[:active], queue_adapter: queue_adapter)
26
+ end
27
+ end
28
+ end
29
+
30
+ def queue
31
+ self.class.queues[queue_name]
32
+ end
33
+
34
+ # Top-level query methods added to `ActiveJob`
35
+ module Root
36
+ def queues
37
+ ActiveJob::Base.queues
38
+ end
39
+
40
+ def jobs
41
+ ActiveJob::Base.jobs
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,62 @@
1
+ # A queue of jobs
2
+ class ActiveJob::Queue
3
+ attr_reader :name
4
+
5
+ def initialize(name, size: nil, active: nil, queue_adapter: ActiveJob::Base.queue_adapter)
6
+ @name = name
7
+ @queue_adapter = queue_adapter
8
+
9
+ @size = size
10
+ @active = active
11
+ end
12
+
13
+ def size
14
+ @size ||= queue_adapter.queue_size(name)
15
+ end
16
+
17
+ alias length size
18
+
19
+ def clear
20
+ queue_adapter.clear_queue(name)
21
+ end
22
+
23
+ def empty?
24
+ size == 0
25
+ end
26
+
27
+ def pause
28
+ queue_adapter.pause_queue(name)
29
+ end
30
+
31
+ def resume
32
+ queue_adapter.resume_queue(name)
33
+ end
34
+
35
+ def paused?
36
+ !active?
37
+ end
38
+
39
+ def active?
40
+ return @active unless @active.nil?
41
+ @active = !queue_adapter.queue_paused?(name)
42
+ end
43
+
44
+ # Return an +ActiveJob::JobsRelation+ with the pending jobs in the queue.
45
+ def jobs
46
+ ActiveJob::JobsRelation.new(queue_adapter: queue_adapter).pending.where(queue_name: name)
47
+ end
48
+
49
+ def reload
50
+ @active = @size = nil
51
+ self
52
+ end
53
+
54
+ def id
55
+ name.parameterize
56
+ end
57
+
58
+ alias to_param id
59
+
60
+ private
61
+ attr_reader :queue_adapter
62
+ end
@@ -0,0 +1,49 @@
1
+ module ActiveJob::QueueAdapters::AsyncExt
2
+ include MissionControl::Jobs::Adapter
3
+
4
+ # List of filters supported natively. Non-supported filters are done in memory.
5
+ def supported_job_filters(jobs_relation)
6
+ []
7
+ end
8
+
9
+ def supports_queue_pausing?
10
+ false
11
+ end
12
+
13
+ def queues
14
+ []
15
+ end
16
+
17
+ def queue_size(*)
18
+ 0
19
+ end
20
+
21
+ def clear_queue(*)
22
+ end
23
+
24
+ def jobs_count(*)
25
+ 0
26
+ end
27
+
28
+ def fetch_jobs(*)
29
+ []
30
+ end
31
+
32
+ def retry_all_jobs(*)
33
+ end
34
+
35
+ def retry_job(job, *)
36
+ end
37
+
38
+ def discard_all_jobs(*)
39
+ end
40
+
41
+ def discard_job(*)
42
+ end
43
+
44
+ def dispatch_job(*)
45
+ end
46
+
47
+ def find_job(*)
48
+ end
49
+ end