flight_control 2.0.0.beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +12 -0
- data/MIT-LICENSE +20 -0
- data/README.md +265 -0
- data/Rakefile +6 -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 +41 -0
- data/lib/active_job/execution_error.rb +8 -0
- data/lib/active_job/failed.rb +7 -0
- data/lib/active_job/job_proxy.rb +39 -0
- data/lib/active_job/jobs_relation.rb +321 -0
- data/lib/active_job/querying.rb +45 -0
- data/lib/active_job/queue.rb +63 -0
- data/lib/active_job/queue_adapters/solid_queue_ext/recurring_tasks.rb +53 -0
- data/lib/active_job/queue_adapters/solid_queue_ext/workers.rb +42 -0
- data/lib/active_job/queue_adapters/solid_queue_ext.rb +309 -0
- data/lib/active_job/queues.rb +34 -0
- data/lib/flight_control/adapter.rb +155 -0
- data/lib/flight_control/application.rb +20 -0
- data/lib/flight_control/applications.rb +8 -0
- data/lib/flight_control/arguments_filter.rb +25 -0
- data/lib/flight_control/authentication.rb +68 -0
- data/lib/flight_control/console/connect_to.rb +15 -0
- data/lib/flight_control/console/context.rb +11 -0
- data/lib/flight_control/console/jobs_help.rb +23 -0
- data/lib/flight_control/engine.rb +102 -0
- data/lib/flight_control/errors/incompatible_adapter.rb +2 -0
- data/lib/flight_control/errors/resource_not_found.rb +2 -0
- data/lib/flight_control/errors/unsupported_adapter.rb +2 -0
- data/lib/flight_control/i18n_config.rb +17 -0
- data/lib/flight_control/identified_by_name.rb +18 -0
- data/lib/flight_control/identified_elements.rb +24 -0
- data/lib/flight_control/server/recurring_tasks.rb +15 -0
- data/lib/flight_control/server/serializable.rb +24 -0
- data/lib/flight_control/server/workers.rb +13 -0
- data/lib/flight_control/server.rb +33 -0
- data/lib/flight_control/tasks.rb +6 -0
- data/lib/flight_control/version.rb +3 -0
- data/lib/flight_control/workers_relation.rb +79 -0
- data/lib/flight_control.rb +39 -0
- metadata +377 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
module ActiveJob::QueueAdapters::SolidQueueExt
|
|
2
|
+
include FlightControl::Adapter
|
|
3
|
+
include Workers
|
|
4
|
+
include RecurringTasks
|
|
5
|
+
|
|
6
|
+
def queues
|
|
7
|
+
queues = SolidQueue::Queue.all
|
|
8
|
+
pauses = SolidQueue::Pause.where(queue_name: queues.map(&:name)).index_by(&:queue_name)
|
|
9
|
+
|
|
10
|
+
queues.collect do |queue|
|
|
11
|
+
{
|
|
12
|
+
name: queue.name,
|
|
13
|
+
size: queue.size,
|
|
14
|
+
active: pauses[queue.name].nil?
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def queue_size(queue_name)
|
|
20
|
+
find_queue_by_name(queue_name).size
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def clear_queue(queue_name)
|
|
24
|
+
find_queue_by_name(queue_name).clear
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def pause_queue(queue_name)
|
|
28
|
+
find_queue_by_name(queue_name).pause
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def resume_queue(queue_name)
|
|
32
|
+
find_queue_by_name(queue_name).resume
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def queue_paused?(queue_name)
|
|
36
|
+
find_queue_by_name(queue_name).paused?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def supported_job_statuses
|
|
40
|
+
SolidQueueJobs::STATUS_MAP.keys
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def supported_job_filters(*)
|
|
44
|
+
[:queue_name, :job_class_name, :finished_at]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def jobs_count(jobs_relation)
|
|
48
|
+
SolidQueueJobs.new(jobs_relation).count
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fetch_jobs(jobs_relation)
|
|
52
|
+
SolidQueueJobs.new(jobs_relation).jobs.map do |job|
|
|
53
|
+
deserialize_and_proxy_solid_queue_job(job, jobs_relation.status)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def retry_all_jobs(jobs_relation)
|
|
58
|
+
SolidQueueJobs.new(jobs_relation).retry_all
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def retry_job(job, jobs_relation)
|
|
62
|
+
find_solid_queue_job!(job.job_id, jobs_relation).retry
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def discard_all_jobs(jobs_relation)
|
|
66
|
+
SolidQueueJobs.new(jobs_relation).discard_all
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def discard_job(job, jobs_relation)
|
|
70
|
+
find_solid_queue_job!(job.job_id, jobs_relation).discard
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def dispatch_job(job, jobs_relation)
|
|
74
|
+
dispatch_immediately find_solid_queue_job!(job.job_id, jobs_relation)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def find_job(job_id, *)
|
|
78
|
+
if (job = SolidQueue::Job.where(active_job_id: job_id).order(:id).last)
|
|
79
|
+
deserialize_and_proxy_solid_queue_job job
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
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"] = FlightControl.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
|
+
end
|
|
133
|
+
|
|
134
|
+
def dispatch_immediately(job)
|
|
135
|
+
if job.blocked?
|
|
136
|
+
SolidQueue::Job.transaction do
|
|
137
|
+
job.dispatch_bypassing_concurrency_limits
|
|
138
|
+
job.blocked_execution.destroy!
|
|
139
|
+
end
|
|
140
|
+
else
|
|
141
|
+
job.scheduled_execution.update!(scheduled_at: Time.now)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
class SolidQueueJobs
|
|
146
|
+
STATUS_MAP = {
|
|
147
|
+
pending: :ready,
|
|
148
|
+
failed: :failed,
|
|
149
|
+
in_progress: :claimed,
|
|
150
|
+
blocked: :blocked,
|
|
151
|
+
scheduled: :scheduled,
|
|
152
|
+
finished: :finished
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
def initialize(jobs_relation)
|
|
156
|
+
@jobs_relation = jobs_relation
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def jobs
|
|
160
|
+
solid_queue_status.finished? ? order_finished_jobs(finished_jobs) : order_executions(executions).map(&:job).compact
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def count
|
|
164
|
+
limit_value_provided? ? direct_count : internally_limited_count
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def find_job(active_job_id)
|
|
168
|
+
if (job = SolidQueue::Job.where(active_job_id: active_job_id).order(:id).last)
|
|
169
|
+
job if matches_relation_filters?(job)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def discard_all
|
|
174
|
+
execution_class_by_status.discard_all_from_jobs(jobs)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def retry_all
|
|
178
|
+
SolidQueue::FailedExecution.retry_all(jobs)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
attr_reader :jobs_relation
|
|
184
|
+
|
|
185
|
+
delegate :queue_name, :limit_value, :limit_value_provided?, :offset_value, :job_class_name,
|
|
186
|
+
:default_page_size, :worker_id, :recurring_task_id, :finished_at, to: :jobs_relation
|
|
187
|
+
|
|
188
|
+
def executions
|
|
189
|
+
execution_class_by_status
|
|
190
|
+
.then { |executions| include_execution_association(executions) }
|
|
191
|
+
.then { |executions| filter_executions_by_queue(executions) }
|
|
192
|
+
.then { |executions| filter_executions_by_class(executions) }
|
|
193
|
+
.then { |executions| filter_executions_by_process_id(executions) }
|
|
194
|
+
.then { |executions| filter_executions_by_task_key(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| filter_jobs_by_finished_at(jobs) }
|
|
204
|
+
.then { |jobs| limit(jobs) }
|
|
205
|
+
.then { |jobs| offset(jobs) }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def order_finished_jobs(jobs)
|
|
209
|
+
jobs.order(finished_at: :desc)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def order_executions(executions)
|
|
213
|
+
# Follow polling order for scheduled executions, the rest newest first
|
|
214
|
+
if solid_queue_status.scheduled?
|
|
215
|
+
executions.ordered
|
|
216
|
+
else
|
|
217
|
+
executions.order(job_id: :desc)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def matches_relation_filters?(job)
|
|
222
|
+
matches_status?(job) && matches_queue_name?(job)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def direct_count
|
|
226
|
+
solid_queue_status.finished? ? finished_jobs.count : executions.count
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def internally_limited_count
|
|
230
|
+
count_limit = FlightControl.internal_query_count_limit + 1
|
|
231
|
+
limited_count = solid_queue_status.finished? ? finished_jobs.limit(count_limit).count : executions.limit(count_limit).count
|
|
232
|
+
(limited_count == count_limit) ? Float::INFINITY : limited_count
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def execution_class_by_status
|
|
236
|
+
if recurring_task_id.present?
|
|
237
|
+
SolidQueue::RecurringExecution
|
|
238
|
+
elsif solid_queue_status.present? && !solid_queue_status.finished?
|
|
239
|
+
"SolidQueue::#{solid_queue_status.capitalize}Execution".safe_constantize
|
|
240
|
+
else
|
|
241
|
+
raise ActiveJob::Errors::QueryError, "Status not supported: #{jobs_relation.status}"
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def include_execution_association(executions)
|
|
246
|
+
solid_queue_status.present? ? executions.includes(job: "#{solid_queue_status}_execution") : executions.includes(:job)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def filter_executions_by_queue(executions)
|
|
250
|
+
return executions unless queue_name.present?
|
|
251
|
+
|
|
252
|
+
if solid_queue_status.ready?
|
|
253
|
+
executions.where(queue_name: queue_name)
|
|
254
|
+
else
|
|
255
|
+
executions.where(job: {queue_name: queue_name})
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def filter_jobs_by_queue(jobs)
|
|
260
|
+
queue_name.present? ? jobs.where(queue_name: queue_name) : jobs
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def filter_executions_by_class(executions)
|
|
264
|
+
job_class_name.present? ? executions.where(job: {class_name: job_class_name}) : executions
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def filter_executions_by_process_id(executions)
|
|
268
|
+
return executions unless worker_id.present?
|
|
269
|
+
|
|
270
|
+
if solid_queue_status.claimed?
|
|
271
|
+
executions.where(process_id: worker_id)
|
|
272
|
+
else
|
|
273
|
+
raise ActiveJob::Errors::QueryError, "Filtering by worker id is not supported for status #{jobs_relation.status}"
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def filter_executions_by_task_key(executions)
|
|
278
|
+
recurring_task_id.present? ? executions.where(task_key: recurring_task_id) : executions
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def filter_jobs_by_class(jobs)
|
|
282
|
+
job_class_name.present? ? jobs.where(class_name: job_class_name) : jobs
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def filter_jobs_by_finished_at(jobs)
|
|
286
|
+
finished_at.present? ? jobs.where(finished_at: finished_at) : jobs
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def limit(executions_or_jobs)
|
|
290
|
+
limit_value.present? ? executions_or_jobs.limit(limit_value) : executions_or_jobs
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def offset(executions_or_jobs)
|
|
294
|
+
offset_value.present? ? executions_or_jobs.offset(offset_value) : executions_or_jobs
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def matches_status?(job)
|
|
298
|
+
solid_queue_status.blank? || job.public_send("#{solid_queue_status}?")
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def matches_queue_name?(job)
|
|
302
|
+
queue_name.blank? || job.queue_name == queue_name
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def solid_queue_status
|
|
306
|
+
STATUS_MAP[jobs_relation.status].to_s.inquiry
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
|
|
33
|
+
attr_reader :queues_by_id
|
|
34
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
module FlightControl::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
|
+
# Returns an array with the list of workers. Each worker is represented as a hash
|
|
59
|
+
# with these attributes:
|
|
60
|
+
# {
|
|
61
|
+
# id: 123,
|
|
62
|
+
# name: "worker-name",
|
|
63
|
+
# hostname: "hey-default-101",
|
|
64
|
+
# last_heartbeat_at: Fri, 26 Jan 2024 20:31:09.652174000 UTC +00:00,
|
|
65
|
+
# configuration: { ... }
|
|
66
|
+
# raw_data: { ... }
|
|
67
|
+
# }
|
|
68
|
+
def workers
|
|
69
|
+
if exposes_workers?
|
|
70
|
+
raise_incompatible_adapter_error_from :workers
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Returns a worker represented by a hash as indicated above
|
|
75
|
+
def find_worker(worker_id)
|
|
76
|
+
if exposes_workers?
|
|
77
|
+
raise_incompatible_adapter_error_from :find_worker
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns an array with the list of queues. Each queue is represented as a hash
|
|
82
|
+
# with these attributes:
|
|
83
|
+
# {
|
|
84
|
+
# name: "queue_name",
|
|
85
|
+
# size: 1,
|
|
86
|
+
# active: true
|
|
87
|
+
# }
|
|
88
|
+
def queues
|
|
89
|
+
raise_incompatible_adapter_error_from :queue_names
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def queue_size(queue_name)
|
|
93
|
+
raise_incompatible_adapter_error_from :queue_size
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def clear_queue(queue_name)
|
|
97
|
+
raise_incompatible_adapter_error_from :clear_queue
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def pause_queue(queue_name)
|
|
101
|
+
if supports_queue_pausing?
|
|
102
|
+
raise_incompatible_adapter_error_from :pause_queue
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def resume_queue(queue_name)
|
|
107
|
+
if supports_queue_pausing?
|
|
108
|
+
raise_incompatible_adapter_error_from :resume_queue
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def queue_paused?(queue_name)
|
|
113
|
+
if supports_queue_pausing?
|
|
114
|
+
raise_incompatible_adapter_error_from :queue_paused?
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def jobs_count(jobs_relation)
|
|
119
|
+
raise_incompatible_adapter_error_from :jobs_count
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def fetch_jobs(jobs_relation)
|
|
123
|
+
raise_incompatible_adapter_error_from :fetch_jobs
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def retry_all_jobs(jobs_relation)
|
|
127
|
+
raise_incompatible_adapter_error_from :retry_all_jobs
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def retry_job(job, jobs_relation)
|
|
131
|
+
raise_incompatible_adapter_error_from :retry_job
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def discard_all_jobs(jobs_relation)
|
|
135
|
+
raise_incompatible_adapter_error_from :discard_all_jobs
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def discard_job(job, jobs_relation)
|
|
139
|
+
raise_incompatible_adapter_error_from :discard_job
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def dispatch_job(job, jobs_relation)
|
|
143
|
+
raise_incompatible_adapter_error_from :dispatch_job
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def find_job(job_id, *)
|
|
147
|
+
raise_incompatible_adapter_error_from :find_job
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def raise_incompatible_adapter_error_from(method_name)
|
|
153
|
+
raise FlightControl::Errors::IncompatibleAdapter, "Adapter #{ActiveJob.adapter_name(self)} must implement `#{method_name}`"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# An application containing backend jobs servers
|
|
2
|
+
class FlightControl::Application
|
|
3
|
+
include FlightControl::IdentifiedByName
|
|
4
|
+
|
|
5
|
+
attr_reader :servers
|
|
6
|
+
|
|
7
|
+
def initialize(name:)
|
|
8
|
+
super
|
|
9
|
+
@servers = FlightControl::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 << FlightControl::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 FlightControl::Applications < FlightControl::IdentifiedElements
|
|
3
|
+
def add(name, queue_adapters_by_name = {})
|
|
4
|
+
self << FlightControl::Application.new(name: name).tap do |application|
|
|
5
|
+
application.add_servers(queue_adapters_by_name)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Replaces argument values with [FILTERED] for any keys that match a filter.
|
|
2
|
+
class FlightControl::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
|
+
|
|
24
|
+
attr_reader :filter
|
|
25
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require "rails/command"
|
|
2
|
+
|
|
3
|
+
class FlightControl::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 Flight Control 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
|
+
|
|
36
|
+
attr_reader :environment
|
|
37
|
+
|
|
38
|
+
def credentials_accessible?
|
|
39
|
+
credentials.read.present?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def authentication_configured?
|
|
43
|
+
%i[http_basic_auth_user http_basic_auth_password].any? do |key|
|
|
44
|
+
credentials.dig(:flight_control, key).present?
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def store_credentials(username, password)
|
|
49
|
+
content = credentials.read + "\n" + http_authentication_entry(username, password) + "\n"
|
|
50
|
+
credentials.write(content)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def credentials
|
|
54
|
+
@credentials ||= Rails.application.encrypted(config.content_path, key_path: config.key_path)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def config
|
|
58
|
+
Rails.application.config.credentials
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def http_authentication_entry(username, password)
|
|
62
|
+
<<~ENTRY
|
|
63
|
+
flight_control:
|
|
64
|
+
http_basic_auth_user: #{username}
|
|
65
|
+
http_basic_auth_password: #{password}
|
|
66
|
+
ENTRY
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "irb/command"
|
|
2
|
+
|
|
3
|
+
module FlightControl::Console
|
|
4
|
+
class ConnectTo < IRB::Command::Base
|
|
5
|
+
category "Flight Control jobs"
|
|
6
|
+
description "Connect to a job server"
|
|
7
|
+
|
|
8
|
+
def execute(server_locator)
|
|
9
|
+
server = FlightControl::Server.from_global_id(server_locator)
|
|
10
|
+
FlightControl::Current.server = server
|
|
11
|
+
|
|
12
|
+
puts "Connected to #{server_locator}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require "irb/command"
|
|
2
|
+
|
|
3
|
+
module FlightControl::Console
|
|
4
|
+
class JobsHelp < IRB::Command::Base
|
|
5
|
+
category "Flight Control jobs"
|
|
6
|
+
description "Show help for managing jobs"
|
|
7
|
+
|
|
8
|
+
def execute(*)
|
|
9
|
+
puts "You are currently connected to #{FlightControl::Current.server}" if FlightControl::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
|
+
FlightControl.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
|