rails_console_pro 0.1.1 → 0.1.3
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 +4 -4
- data/.rspec_status +261 -240
- data/CHANGELOG.md +4 -0
- data/QUICK_START.md +8 -0
- data/README.md +16 -0
- data/docs/FORMATTING.md +5 -0
- data/docs/MODEL_STATISTICS.md +4 -0
- data/docs/OBJECT_DIFFING.md +6 -0
- data/docs/PROFILING.md +91 -0
- data/docs/QUEUE_INSIGHTS.md +82 -0
- data/docs/SCHEMA_INSPECTION.md +5 -0
- data/docs/SNIPPETS.md +71 -0
- data/lib/rails_console_pro/commands/base_command.rb +1 -1
- data/lib/rails_console_pro/commands/jobs_command.rb +212 -0
- data/lib/rails_console_pro/commands/profile_command.rb +84 -0
- data/lib/rails_console_pro/commands/snippets_command.rb +141 -0
- data/lib/rails_console_pro/commands.rb +15 -0
- data/lib/rails_console_pro/configuration.rb +39 -0
- data/lib/rails_console_pro/format_exporter.rb +8 -0
- data/lib/rails_console_pro/global_methods.rb +12 -0
- data/lib/rails_console_pro/initializer.rb +29 -4
- data/lib/rails_console_pro/model_validator.rb +1 -1
- data/lib/rails_console_pro/printers/profile_printer.rb +180 -0
- data/lib/rails_console_pro/printers/queue_insights_printer.rb +150 -0
- data/lib/rails_console_pro/printers/snippet_collection_printer.rb +68 -0
- data/lib/rails_console_pro/printers/snippet_printer.rb +64 -0
- data/lib/rails_console_pro/profile_result.rb +109 -0
- data/lib/rails_console_pro/pry_commands.rb +106 -0
- data/lib/rails_console_pro/queue_insights_result.rb +110 -0
- data/lib/rails_console_pro/serializers/profile_serializer.rb +73 -0
- data/lib/rails_console_pro/services/profile_collector.rb +245 -0
- data/lib/rails_console_pro/services/queue_action_service.rb +176 -0
- data/lib/rails_console_pro/services/queue_insight_fetcher.rb +600 -0
- data/lib/rails_console_pro/services/snippet_repository.rb +191 -0
- data/lib/rails_console_pro/snippets/collection_result.rb +44 -0
- data/lib/rails_console_pro/snippets/single_result.rb +30 -0
- data/lib/rails_console_pro/snippets/snippet.rb +112 -0
- data/lib/rails_console_pro/snippets.rb +12 -0
- data/lib/rails_console_pro/version.rb +1 -1
- data/rails_console_pro.gemspec +1 -1
- metadata +26 -8
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Services
|
|
5
|
+
class QueueInsightFetcher
|
|
6
|
+
DEFAULT_LIMIT = 20
|
|
7
|
+
|
|
8
|
+
def initialize(queue_adapter = nil)
|
|
9
|
+
@queue_adapter = queue_adapter
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def fetch(limit: DEFAULT_LIMIT, queue: nil)
|
|
13
|
+
adapter = build_adapter(@queue_adapter || detect_active_job_adapter)
|
|
14
|
+
return nil unless adapter
|
|
15
|
+
|
|
16
|
+
safe_limit = normalize_limit(limit)
|
|
17
|
+
adapter.fetch(limit: safe_limit, queue: queue)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def detect_active_job_adapter
|
|
23
|
+
return unless defined?(ActiveJob::Base)
|
|
24
|
+
|
|
25
|
+
ActiveJob::Base.queue_adapter
|
|
26
|
+
rescue StandardError
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def build_adapter(queue_adapter)
|
|
31
|
+
if sidekiq_adapter?(queue_adapter)
|
|
32
|
+
Adapters::Sidekiq.new(queue_adapter)
|
|
33
|
+
elsif solid_queue_adapter?(queue_adapter)
|
|
34
|
+
Adapters::SolidQueue.new(queue_adapter)
|
|
35
|
+
elsif sidekiq_present?
|
|
36
|
+
Adapters::Sidekiq.new(queue_adapter)
|
|
37
|
+
elsif solid_queue_present?
|
|
38
|
+
Adapters::SolidQueue.new(queue_adapter)
|
|
39
|
+
elsif queue_adapter
|
|
40
|
+
Adapters::Generic.new(queue_adapter)
|
|
41
|
+
else
|
|
42
|
+
Adapters::Generic.new(ActiveJob::Base.queue_adapter) if active_job_loaded?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def sidekiq_adapter?(adapter)
|
|
47
|
+
adapter.class.name == 'ActiveJob::QueueAdapters::SidekiqAdapter' &&
|
|
48
|
+
defined?(::Sidekiq)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def solid_queue_adapter?(adapter)
|
|
52
|
+
adapter.class.name == 'ActiveJob::QueueAdapters::SolidQueueAdapter' &&
|
|
53
|
+
defined?(::SolidQueue)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def sidekiq_present?
|
|
57
|
+
defined?(::Sidekiq) && defined?(::Sidekiq::Queue)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def solid_queue_present?
|
|
61
|
+
defined?(::SolidQueue) && defined?(::SolidQueue::Job)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def active_job_loaded?
|
|
65
|
+
defined?(ActiveJob::Base)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def normalize_limit(limit)
|
|
69
|
+
value = limit.to_i
|
|
70
|
+
return DEFAULT_LIMIT if value <= 0
|
|
71
|
+
|
|
72
|
+
[value, 200].min
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
module Adapters
|
|
76
|
+
class Base
|
|
77
|
+
attr_reader :queue_adapter
|
|
78
|
+
|
|
79
|
+
def initialize(queue_adapter)
|
|
80
|
+
@queue_adapter = queue_adapter
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def fetch(limit:, queue:)
|
|
84
|
+
payload = gather(limit: limit, queue: queue)
|
|
85
|
+
build_result(**payload.merge(limit: limit))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
protected
|
|
89
|
+
|
|
90
|
+
def gather(limit:, queue:)
|
|
91
|
+
{
|
|
92
|
+
enqueued_jobs: [],
|
|
93
|
+
retry_jobs: [],
|
|
94
|
+
recent_executions: [],
|
|
95
|
+
meta: {},
|
|
96
|
+
warnings: []
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def build_result(enqueued_jobs:, retry_jobs:, recent_executions:, meta:, warnings:, limit:)
|
|
101
|
+
QueueInsightsResult.new(
|
|
102
|
+
adapter_name: adapter_name,
|
|
103
|
+
adapter_type: adapter_type,
|
|
104
|
+
enqueued_jobs: Array(enqueued_jobs).first(limit),
|
|
105
|
+
retry_jobs: Array(retry_jobs).first(limit),
|
|
106
|
+
recent_executions: Array(recent_executions).first(limit),
|
|
107
|
+
meta: meta || {},
|
|
108
|
+
warnings: Array(warnings),
|
|
109
|
+
captured_at: current_time
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def adapter_name
|
|
114
|
+
queue_adapter.class.name
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def adapter_type
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def job_summary(**attrs)
|
|
122
|
+
QueueInsightsResult::JobSummary.new(**attrs)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def execution_summary(**attrs)
|
|
126
|
+
QueueInsightsResult::ExecutionSummary.new(**attrs)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def current_time
|
|
130
|
+
if Time.respond_to?(:zone) && Time.zone
|
|
131
|
+
Time.zone.now
|
|
132
|
+
else
|
|
133
|
+
Time.now
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def safe_execute(warnings, default: nil)
|
|
138
|
+
yield
|
|
139
|
+
rescue StandardError => e
|
|
140
|
+
warnings << "#{adapter_name}: #{e.message}"
|
|
141
|
+
default
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def present?(value)
|
|
145
|
+
case value
|
|
146
|
+
when nil
|
|
147
|
+
false
|
|
148
|
+
when String
|
|
149
|
+
!value.empty?
|
|
150
|
+
else
|
|
151
|
+
value.respond_to?(:empty?) ? !value.empty? : true
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
class Sidekiq < Base
|
|
157
|
+
def adapter_name
|
|
158
|
+
"Sidekiq"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def adapter_type
|
|
162
|
+
"ActiveJob Adapter"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
protected
|
|
166
|
+
|
|
167
|
+
def gather(limit:, queue:)
|
|
168
|
+
warnings = []
|
|
169
|
+
unless ensure_sidekiq_api_loaded
|
|
170
|
+
warnings << "Sidekiq API is not available. Require 'sidekiq/api' in your console session."
|
|
171
|
+
return {
|
|
172
|
+
enqueued_jobs: [],
|
|
173
|
+
retry_jobs: [],
|
|
174
|
+
recent_executions: [],
|
|
175
|
+
meta: {},
|
|
176
|
+
warnings: warnings
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
{
|
|
180
|
+
enqueued_jobs: safe_execute(warnings, default: []) { fetch_enqueued_jobs(limit, queue) },
|
|
181
|
+
retry_jobs: safe_execute(warnings, default: []) { fetch_retry_jobs(limit, queue) },
|
|
182
|
+
recent_executions: safe_execute(warnings, default: []) { fetch_recent_executions(limit, queue) },
|
|
183
|
+
meta: safe_execute(warnings, default: {}) { fetch_meta },
|
|
184
|
+
warnings: warnings
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
def ensure_sidekiq_api_loaded
|
|
191
|
+
return true if defined?(::Sidekiq::Queue)
|
|
192
|
+
|
|
193
|
+
require 'sidekiq/api'
|
|
194
|
+
defined?(::Sidekiq::Queue)
|
|
195
|
+
rescue LoadError
|
|
196
|
+
false
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def fetch_enqueued_jobs(limit, queue)
|
|
200
|
+
queues = sidekiq_queues(queue)
|
|
201
|
+
jobs = []
|
|
202
|
+
|
|
203
|
+
queues.each do |sidekiq_queue|
|
|
204
|
+
sidekiq_queue.each do |job|
|
|
205
|
+
jobs << build_sidekiq_job(job)
|
|
206
|
+
break if jobs.size >= limit
|
|
207
|
+
end
|
|
208
|
+
break if jobs.size >= limit
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
jobs.compact
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def fetch_retry_jobs(limit, queue)
|
|
215
|
+
return [] unless defined?(::Sidekiq::RetrySet)
|
|
216
|
+
|
|
217
|
+
::Sidekiq::RetrySet.new.take(limit).map do |job|
|
|
218
|
+
next if queue && job.queue != queue
|
|
219
|
+
|
|
220
|
+
build_sidekiq_job(job)
|
|
221
|
+
end.compact
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def fetch_recent_executions(limit, queue)
|
|
225
|
+
return [] unless defined?(::Sidekiq::Workers)
|
|
226
|
+
|
|
227
|
+
workers = ::Sidekiq::Workers.new
|
|
228
|
+
entries = []
|
|
229
|
+
|
|
230
|
+
workers.each do |process_id, thread_id, work|
|
|
231
|
+
next if queue && work['queue'] != queue
|
|
232
|
+
|
|
233
|
+
entries << build_worker_execution(process_id, thread_id, work)
|
|
234
|
+
break if entries.size >= limit
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
entries
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def fetch_meta
|
|
241
|
+
return {} unless defined?(::Sidekiq::Stats)
|
|
242
|
+
|
|
243
|
+
stats = ::Sidekiq::Stats.new
|
|
244
|
+
|
|
245
|
+
{
|
|
246
|
+
enqueued: stats.enqueued,
|
|
247
|
+
processed: stats.processed,
|
|
248
|
+
failed: stats.failed,
|
|
249
|
+
retries: stats.retry_size,
|
|
250
|
+
scheduled: stats.scheduled_size,
|
|
251
|
+
dead: stats.dead_size
|
|
252
|
+
}
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def sidekiq_queues(queue)
|
|
256
|
+
if queue
|
|
257
|
+
[::Sidekiq::Queue.new(queue)]
|
|
258
|
+
elsif ::Sidekiq::Queue.respond_to?(:all)
|
|
259
|
+
::Sidekiq::Queue.all
|
|
260
|
+
else
|
|
261
|
+
[::Sidekiq::Queue.new("default")]
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def build_sidekiq_job(job)
|
|
266
|
+
item = job.respond_to?(:item) ? job.item : {}
|
|
267
|
+
job_class = job.respond_to?(:display_class) ? job.display_class : item['class'] || job.klass rescue nil
|
|
268
|
+
args = job.respond_to?(:args) ? job.args : item['args']
|
|
269
|
+
job_id = job.respond_to?(:jid) ? job.jid : item['jid']
|
|
270
|
+
|
|
271
|
+
job_summary(
|
|
272
|
+
id: job_id,
|
|
273
|
+
job_class: job_class,
|
|
274
|
+
queue: job.respond_to?(:queue) ? job.queue : item['queue'],
|
|
275
|
+
args: args,
|
|
276
|
+
enqueued_at: extract_timestamp(job, item, 'enqueued_at'),
|
|
277
|
+
scheduled_at: extract_timestamp(job, item, 'at'),
|
|
278
|
+
attempts: item['retry_count'] || item['attempts'],
|
|
279
|
+
error: item['error_message'],
|
|
280
|
+
metadata: build_job_metadata(item)
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def build_worker_execution(process_id, thread_id, work)
|
|
285
|
+
payload = work['payload'] || {}
|
|
286
|
+
started_at = work['run_at']
|
|
287
|
+
runtime_ms = if started_at
|
|
288
|
+
(current_time.to_f - started_at.to_f) * 1000.0
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
execution_summary(
|
|
292
|
+
id: payload['jid'] || "#{process_id}:#{thread_id}",
|
|
293
|
+
job_class: payload['class'],
|
|
294
|
+
queue: work['queue'],
|
|
295
|
+
started_at: started_at,
|
|
296
|
+
runtime_ms: runtime_ms,
|
|
297
|
+
worker: payload['worker'],
|
|
298
|
+
hostname: process_id,
|
|
299
|
+
metadata: {
|
|
300
|
+
thread: thread_id,
|
|
301
|
+
tags: payload['tags']
|
|
302
|
+
}.compact
|
|
303
|
+
)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def extract_timestamp(job, item, key)
|
|
307
|
+
return job.public_send(key) if job.respond_to?(key)
|
|
308
|
+
item[key] || item[key.to_s]
|
|
309
|
+
rescue StandardError
|
|
310
|
+
nil
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def build_job_metadata(item)
|
|
314
|
+
metadata = {}
|
|
315
|
+
metadata[:wrapped] = item['wrapped'] if item['wrapped']
|
|
316
|
+
metadata[:priority] = item['priority'] if item.key?('priority')
|
|
317
|
+
metadata[:queue_latency_ms] = (item['enqueued_at'] && item['created_at']) ? (item['created_at'] - item['enqueued_at']) * 1000.0 : nil
|
|
318
|
+
metadata.compact
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
class SolidQueue < Base
|
|
323
|
+
def adapter_name
|
|
324
|
+
"SolidQueue"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def adapter_type
|
|
328
|
+
"ActiveJob Adapter"
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
protected
|
|
332
|
+
|
|
333
|
+
def gather(limit:, queue:)
|
|
334
|
+
warnings = []
|
|
335
|
+
{
|
|
336
|
+
enqueued_jobs: safe_execute(warnings, default: []) { fetch_ready_jobs(limit, queue) },
|
|
337
|
+
retry_jobs: safe_execute(warnings, default: []) { fetch_retry_jobs(limit, queue) },
|
|
338
|
+
recent_executions: safe_execute(warnings, default: []) { fetch_recent_executions(limit, queue) },
|
|
339
|
+
meta: safe_execute(warnings, default: {}) { fetch_meta },
|
|
340
|
+
warnings: warnings
|
|
341
|
+
}
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
private
|
|
345
|
+
|
|
346
|
+
def fetch_ready_jobs(limit, queue)
|
|
347
|
+
relation = solid_queue_jobs_scope(:ready)
|
|
348
|
+
relation = apply_queue_filter(relation, queue)
|
|
349
|
+
sample_relation(relation, limit).map { |job| build_solid_queue_job(job) }
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def fetch_retry_jobs(limit, queue)
|
|
353
|
+
relation = if SolidQueue::Job.respond_to?(:retryable)
|
|
354
|
+
SolidQueue::Job.retryable
|
|
355
|
+
elsif SolidQueue::Job.respond_to?(:failed)
|
|
356
|
+
SolidQueue::Job.failed
|
|
357
|
+
else
|
|
358
|
+
nil
|
|
359
|
+
end
|
|
360
|
+
return [] unless relation
|
|
361
|
+
|
|
362
|
+
relation = apply_queue_filter(relation, queue)
|
|
363
|
+
sample_relation(relation, limit).map { |job| build_solid_queue_job(job) }
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def fetch_recent_executions(limit, queue)
|
|
367
|
+
execution_relation = solid_queue_execution_relation(queue)
|
|
368
|
+
return [] unless execution_relation
|
|
369
|
+
|
|
370
|
+
sample_relation(execution_relation, limit).map { |execution| build_execution(execution) }
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def fetch_meta
|
|
374
|
+
if defined?(SolidQueue::Statistics) && SolidQueue::Statistics.respond_to?(:snapshot)
|
|
375
|
+
SolidQueue::Statistics.snapshot.slice(
|
|
376
|
+
:ready_jobs,
|
|
377
|
+
:scheduled_jobs,
|
|
378
|
+
:running_jobs,
|
|
379
|
+
:retryable_jobs,
|
|
380
|
+
:failed_jobs
|
|
381
|
+
)
|
|
382
|
+
else
|
|
383
|
+
{}
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def solid_queue_jobs_scope(scope_name)
|
|
388
|
+
if SolidQueue::Job.respond_to?(scope_name)
|
|
389
|
+
SolidQueue::Job.public_send(scope_name)
|
|
390
|
+
elsif SolidQueue::Job.respond_to?(:where)
|
|
391
|
+
SolidQueue::Job.where(state: scope_name.to_s)
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def apply_queue_filter(relation, queue)
|
|
396
|
+
return relation unless queue && relation
|
|
397
|
+
|
|
398
|
+
if relation.respond_to?(:for_queue)
|
|
399
|
+
relation.for_queue(queue)
|
|
400
|
+
elsif relation.respond_to?(:where)
|
|
401
|
+
relation.where(queue_name: queue)
|
|
402
|
+
else
|
|
403
|
+
relation
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def sample_relation(relation, limit)
|
|
408
|
+
return [] unless relation
|
|
409
|
+
|
|
410
|
+
if relation.respond_to?(:limit)
|
|
411
|
+
relation.limit(limit).to_a
|
|
412
|
+
elsif relation.respond_to?(:take)
|
|
413
|
+
Array(relation.take(limit))
|
|
414
|
+
else
|
|
415
|
+
Array(relation).first(limit)
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def build_solid_queue_job(job)
|
|
420
|
+
job_summary(
|
|
421
|
+
id: safe_attr(job, :id),
|
|
422
|
+
job_class: safe_attr(job, :class_name) || safe_attr(job, :job_class),
|
|
423
|
+
queue: safe_attr(job, :queue_name) || "default",
|
|
424
|
+
args: safe_attr(job, :arguments),
|
|
425
|
+
enqueued_at: safe_attr(job, :enqueued_at) || safe_attr(job, :created_at),
|
|
426
|
+
scheduled_at: safe_attr(job, :scheduled_at),
|
|
427
|
+
attempts: safe_attr(job, :attempts) || safe_attr(job, :attempt),
|
|
428
|
+
error: safe_attr(job, :last_error),
|
|
429
|
+
metadata: build_job_metadata(job)
|
|
430
|
+
)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def build_job_metadata(job)
|
|
434
|
+
metadata = {}
|
|
435
|
+
metadata[:priority] = safe_attr(job, :priority) if safe_attr(job, :priority)
|
|
436
|
+
metadata[:singleton] = safe_attr(job, :singleton) if safe_attr(job, :singleton)
|
|
437
|
+
metadata.compact
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def solid_queue_execution_relation(queue)
|
|
441
|
+
execution_class = if defined?(SolidQueue::Execution)
|
|
442
|
+
SolidQueue::Execution
|
|
443
|
+
elsif defined?(SolidQueue::CompletedExecution)
|
|
444
|
+
SolidQueue::CompletedExecution
|
|
445
|
+
end
|
|
446
|
+
return unless execution_class
|
|
447
|
+
|
|
448
|
+
relation = if execution_class.respond_to?(:order)
|
|
449
|
+
execution_class.order(created_at: :desc)
|
|
450
|
+
else
|
|
451
|
+
execution_class
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
if queue && relation.respond_to?(:where)
|
|
455
|
+
relation = relation.where(queue_name: queue)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
relation
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def build_execution(execution)
|
|
462
|
+
execution_summary(
|
|
463
|
+
id: safe_attr(execution, :id),
|
|
464
|
+
job_class: safe_attr(execution, :job_class) || safe_attr(execution, :class_name),
|
|
465
|
+
queue: safe_attr(execution, :queue_name),
|
|
466
|
+
started_at: safe_attr(execution, :started_at) || safe_attr(execution, :created_at),
|
|
467
|
+
runtime_ms: extract_runtime(execution),
|
|
468
|
+
worker: safe_attr(execution, :worker_id) || safe_attr(execution, :worker_name),
|
|
469
|
+
hostname: safe_attr(execution, :host),
|
|
470
|
+
metadata: build_execution_metadata(execution)
|
|
471
|
+
)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def build_execution_metadata(execution)
|
|
475
|
+
metadata = {}
|
|
476
|
+
metadata[:status] = safe_attr(execution, :status) if safe_attr(execution, :status)
|
|
477
|
+
metadata[:attempts] = safe_attr(execution, :attempts) if safe_attr(execution, :attempts)
|
|
478
|
+
metadata.compact
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def extract_runtime(execution)
|
|
482
|
+
runtime = safe_attr(execution, :duration) || safe_attr(execution, :duration_ms)
|
|
483
|
+
return runtime if runtime
|
|
484
|
+
|
|
485
|
+
finished_at = safe_attr(execution, :finished_at)
|
|
486
|
+
started_at = safe_attr(execution, :started_at)
|
|
487
|
+
return unless finished_at && started_at
|
|
488
|
+
|
|
489
|
+
((finished_at.to_f - started_at.to_f) * 1000.0).round(2)
|
|
490
|
+
rescue StandardError
|
|
491
|
+
nil
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def safe_attr(object, method_name)
|
|
495
|
+
return unless object.respond_to?(method_name)
|
|
496
|
+
|
|
497
|
+
object.public_send(method_name)
|
|
498
|
+
rescue StandardError
|
|
499
|
+
nil
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
class Generic < Base
|
|
504
|
+
def adapter_type
|
|
505
|
+
"ActiveJob Adapter"
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
protected
|
|
509
|
+
|
|
510
|
+
def gather(limit:, queue:)
|
|
511
|
+
warnings = []
|
|
512
|
+
|
|
513
|
+
{
|
|
514
|
+
enqueued_jobs: safe_execute(warnings, default: []) { collect_active_job_enqueued(limit, queue) },
|
|
515
|
+
retry_jobs: [],
|
|
516
|
+
recent_executions: safe_execute(warnings, default: []) { collect_active_job_performed(limit, queue) },
|
|
517
|
+
meta: {},
|
|
518
|
+
warnings: warnings
|
|
519
|
+
}
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
private
|
|
523
|
+
|
|
524
|
+
def collect_active_job_enqueued(limit, queue)
|
|
525
|
+
return [] unless queue_adapter.respond_to?(:enqueued_jobs)
|
|
526
|
+
|
|
527
|
+
queue_adapter.enqueued_jobs.first(limit).map do |entry|
|
|
528
|
+
build_active_job_entry(entry, queue)
|
|
529
|
+
end.compact
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def collect_active_job_performed(limit, queue)
|
|
533
|
+
return [] unless queue_adapter.respond_to?(:performed_jobs)
|
|
534
|
+
|
|
535
|
+
queue_adapter.performed_jobs.last(limit).reverse.map do |entry|
|
|
536
|
+
build_active_job_execution(entry, queue)
|
|
537
|
+
end.compact
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def build_active_job_entry(entry, queue)
|
|
541
|
+
payload = normalize_payload(entry)
|
|
542
|
+
return if queue && payload[:queue] != queue
|
|
543
|
+
|
|
544
|
+
job_summary(
|
|
545
|
+
id: payload[:job_id],
|
|
546
|
+
job_class: payload[:job_class],
|
|
547
|
+
queue: payload[:queue],
|
|
548
|
+
args: payload[:arguments],
|
|
549
|
+
enqueued_at: payload[:enqueued_at],
|
|
550
|
+
scheduled_at: payload[:scheduled_at],
|
|
551
|
+
metadata: payload[:metadata]
|
|
552
|
+
)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def build_active_job_execution(entry, queue)
|
|
556
|
+
payload = normalize_payload(entry)
|
|
557
|
+
return if queue && payload[:queue] != queue
|
|
558
|
+
|
|
559
|
+
execution_summary(
|
|
560
|
+
id: payload[:job_id],
|
|
561
|
+
job_class: payload[:job_class],
|
|
562
|
+
queue: payload[:queue],
|
|
563
|
+
started_at: payload[:performed_at] || payload[:enqueued_at],
|
|
564
|
+
runtime_ms: payload[:runtime_ms],
|
|
565
|
+
metadata: payload[:metadata]
|
|
566
|
+
)
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def normalize_payload(entry)
|
|
570
|
+
if entry.respond_to?(:to_h)
|
|
571
|
+
entry = entry.to_h
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
{
|
|
575
|
+
job_id: entry[:job_id] || entry[:'job_id'] || entry['job_id'],
|
|
576
|
+
job_class: entry[:job_class] || entry[:'job_class'] || entry['job_class'] || entry[:class] || entry['class'],
|
|
577
|
+
queue: entry[:queue] || entry[:'queue'] || entry['queue'],
|
|
578
|
+
arguments: entry[:arguments] || entry[:args] || entry['arguments'] || entry['args'],
|
|
579
|
+
enqueued_at: entry[:enqueued_at] || entry['enqueued_at'],
|
|
580
|
+
scheduled_at: entry[:scheduled_at] || entry['scheduled_at'],
|
|
581
|
+
performed_at: entry[:performed_at] || entry['performed_at'],
|
|
582
|
+
runtime_ms: entry[:runtime_ms] || entry['runtime_ms'],
|
|
583
|
+
metadata: extract_metadata(entry)
|
|
584
|
+
}
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def extract_metadata(entry)
|
|
588
|
+
meta = {}
|
|
589
|
+
meta[:provider_job_id] = entry[:provider_job_id] || entry['provider_job_id'] if present?(entry[:provider_job_id] || entry['provider_job_id'])
|
|
590
|
+
meta[:priority] = entry[:priority] || entry['priority'] if present?(entry[:priority] || entry['priority'])
|
|
591
|
+
meta[:executions] = entry[:executions] || entry['executions'] if present?(entry[:executions] || entry['executions'])
|
|
592
|
+
meta.compact
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
|