hirefire-resource 0.10.0 → 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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -0
  3. data/LICENSE +21 -66
  4. data/README.md +34 -78
  5. data/hirefire-resource.gemspec +21 -15
  6. data/lib/hirefire/configuration.rb +31 -0
  7. data/lib/hirefire/errors/job_queue_latency_unsupported.rb +12 -0
  8. data/lib/hirefire/errors.rb +9 -0
  9. data/lib/hirefire/hirefire.rb +15 -0
  10. data/lib/hirefire/macro/bunny.rb +55 -55
  11. data/lib/hirefire/macro/delayed_job.rb +80 -60
  12. data/lib/hirefire/macro/deprecated/bunny.rb +85 -0
  13. data/lib/hirefire/macro/deprecated/delayed_job.rb +62 -0
  14. data/lib/hirefire/macro/deprecated/good_job.rb +32 -0
  15. data/lib/hirefire/macro/deprecated/que.rb +76 -0
  16. data/lib/hirefire/macro/deprecated/queue_classic.rb +28 -0
  17. data/lib/hirefire/macro/deprecated/resque.rb +47 -0
  18. data/lib/hirefire/macro/deprecated/sidekiq.rb +138 -0
  19. data/lib/hirefire/macro/good_job.rb +48 -13
  20. data/lib/hirefire/macro/que.rb +56 -36
  21. data/lib/hirefire/macro/queue_classic.rb +86 -0
  22. data/lib/hirefire/macro/resque.rb +112 -24
  23. data/lib/hirefire/macro/sidekiq.rb +324 -74
  24. data/lib/hirefire/macro/solid_queue.rb +166 -0
  25. data/lib/hirefire/middleware.rb +50 -103
  26. data/lib/hirefire/railtie.rb +1 -1
  27. data/lib/hirefire/resource.rb +1 -47
  28. data/lib/hirefire/utility.rb +22 -0
  29. data/lib/hirefire/version.rb +5 -0
  30. data/lib/hirefire/web.rb +151 -0
  31. data/lib/hirefire/worker.rb +39 -0
  32. data/lib/hirefire-resource.rb +4 -9
  33. metadata +50 -24
  34. data/.github/workflows/main.yml +0 -16
  35. data/.gitignore +0 -5
  36. data/Gemfile +0 -10
  37. data/Gemfile.lock +0 -49
  38. data/Rakefile +0 -4
  39. data/lib/hirefire/macro/qc.rb +0 -23
  40. data/lib/hirefire/macro/qu.rb +0 -27
@@ -1,123 +1,373 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest/sha1"
4
+ require_relative "deprecated/sidekiq"
5
+
3
6
  module HireFire
4
7
  module Macro
5
8
  module Sidekiq
9
+ extend HireFire::Macro::Deprecated::Sidekiq
6
10
  extend self
7
11
 
8
- # The latency in seconds for the provided queue.
9
- #
10
- # @example Sidekiq Queue Latency Macro Usage
11
- # HireFire::Macro::Sidekiq.queue # default queue
12
- # HireFire::Macro::Sidekiq.queue("email") # email queue
12
+ # Calculates the maximum job queue latency using Sidekiq. If no queues are specified, it
13
+ # measures latency across all available queues.
13
14
  #
14
- def latency(queue = "default")
15
- ::Sidekiq::Queue.new(queue).latency
15
+ # @param queues [Array<String, Symbol>] (optional) Names of the queues for latency
16
+ # measurement. If not provided, latency is measured across all queues.
17
+ # @param options [Hash] Options to control and filter the latency calculation.
18
+ # @option options [Boolean] :skip_retries (false) If true, skips the RetrySet in latency calculation.
19
+ # @option options [Boolean] :skip_scheduled (false) If true, skips the ScheduledSet in latency calculation.
20
+ # @return [Float] Maximum job queue latency in seconds.
21
+ # @example Calculate latency across all queues
22
+ # HireFire::Macro::Sidekiq.job_queue_latency
23
+ # @example Calculate latency for the "default" queue
24
+ # HireFire::Macro::Sidekiq.job_queue_latency(:default)
25
+ # @example Calculate maximum latency across "default" and "mailer" queues
26
+ # HireFire::Macro::Sidekiq.job_queue_latency(:default, :mailer)
27
+ # @example Calculate latency for the "default" queue, excluding scheduled jobs
28
+ # HireFire::Macro::Sidekiq.job_queue_latency(:default, skip_scheduled: true)
29
+ # @example Calculate latency for the "default" queue, excluding retries
30
+ # HireFire::Macro::Sidekiq.job_queue_latency(:default, skip_retries: true)
31
+ def job_queue_latency(*queues, **options)
32
+ JobQueueLatency.call(*queues, **options)
16
33
  end
17
34
 
18
- # Counts the amount of jobs in the (provided) Sidekiq queue(s).
19
- #
20
- # @example Sidekiq Queue Size Macro Usage
21
- # HireFire::Macro::Sidekiq.queue # all queues
22
- # HireFire::Macro::Sidekiq.queue("email") # only email queue
23
- # HireFire::Macro::Sidekiq.queue("audio", "video") # audio and video queues
24
- # HireFire::Macro::Sidekiq.queue("email", skip_scheduled: true) # only email, will not count scheduled queue
25
- # HireFire::Macro::Sidekiq.queue("audio", skip_retries: true) # only audio, will not count the retries queue
26
- # HireFire::Macro::Sidekiq.queue("audio", skip_working: true) # only audio, will not count already queued
35
+ # Calculates the total job queue size using Sidekiq. If no queues are specified, it measures
36
+ # size across all available queues.
27
37
  #
28
- # @param [Array] queues provide one or more queue names, or none for "all".
29
- # @return [Integer] the number of jobs in the queue(s).
30
- #
31
- def queue(*queues)
32
- require "sidekiq/api"
38
+ # @param queues [Array<String, Symbol>] (optional) Names of the queues for size measurement.
39
+ # If not provided, size is measured across all queues.
40
+ # @param options [Hash] Options to control and filter the count.
41
+ # @option options [Boolean] :server (false) If true, counts jobs server-side using Lua scripting.
42
+ # @option options [Boolean] :skip_retries (false) If true, skips counting jobs in retry queues.
43
+ # @option options [Boolean] :skip_scheduled (false) If true, skips counting jobs in scheduled queues.
44
+ # @option options [Boolean] :skip_working (false) If true, skips counting running jobs.
45
+ # @option options [Integer, nil] :max_scheduled (nil) Max number of scheduled jobs to consider; nil for no limit.
46
+ # @return [Integer] Total job queue size.
47
+ # @example Calculate size across all queues
48
+ # HireFire::Macro::Sidekiq.job_queue_size
49
+ # @example Calculate size for the "default" queue
50
+ # HireFire::Macro::Sidekiq.job_queue_size(:default)
51
+ # @example Calculate size across "default" and "mailer" queues
52
+ # HireFire::Macro::Sidekiq.job_queue_size(:default, :mailer)
53
+ # @example Calculate size for the "default" queue, excluding scheduled jobs
54
+ # HireFire::Macro::Sidekiq.job_queue_size(:default, skip_scheduled: true)
55
+ # @example Calculate size for the "default" queue, excluding retries
56
+ # HireFire::Macro::Sidekiq.job_queue_size(:default, skip_retries: true)
57
+ # @example Calculate size for the "default" queue, excluding running jobs
58
+ # HireFire::Macro::Sidekiq.job_queue_size(:default, skip_working: true)
59
+ # @example Calculate size for the "default" queue using server-side aggregation
60
+ # HireFire::Macro::Sidekiq.job_queue_size(:default, server: true)
61
+ # @example Calculate size for the "default" queue, limiting counting of scheduled jobs to 100_000
62
+ # HireFire::Macro::Sidekiq.job_queue_size(:default, max_scheduled: 100_000)
63
+ def job_queue_size(*queues, **options)
64
+ JobQueueSize.call(*queues, **options)
65
+ end
33
66
 
34
- queues.flatten!
67
+ # @!visibility private
68
+ module Common
69
+ private
35
70
 
36
- options = if queues.last.is_a?(Hash)
37
- queues.pop
38
- else
39
- {}
40
- end
71
+ def find_each_in_set(set)
72
+ cursor = 0
73
+ batch = 1000
74
+
75
+ loop do
76
+ entries = ::Sidekiq.redis do |connection|
77
+ if Gem::Version.new(::Sidekiq::VERSION) >= Gem::Version.new("7.0.0")
78
+ connection.zrange set.name, cursor, cursor + batch - 1, "WITHSCORES"
79
+ else
80
+ connection.zrange set.name, cursor, cursor + batch - 1, withscores: true
81
+ end
82
+ end
83
+
84
+ break if entries.empty?
41
85
 
42
- queues.map!(&:to_s)
43
- all_queues = ::Sidekiq::Queue.all.map(&:name)
44
- queues = all_queues if queues.empty?
86
+ entries.each do |entry, score|
87
+ yield ::Sidekiq::SortedEntry.new(self, score, entry)
88
+ end
45
89
 
46
- if fast_lookup_capable?(queues, all_queues)
47
- fast_lookup(options)
48
- else
49
- dynamic_lookup(queues, options)
90
+ cursor += batch
91
+ end
92
+ end
93
+
94
+ def registered_queues
95
+ ::Sidekiq::Queue.all.map(&:name).to_set
50
96
  end
51
97
  end
52
98
 
53
- private
99
+ # @!visibility private
100
+ module JobQueueLatency
101
+ extend Common
102
+ extend HireFire::Utility
103
+ extend self
54
104
 
55
- def fast_lookup_capable?(queues, all_queues)
56
- # When no queue names are provided (or all of them are), we know we
57
- # can peform much faster counts using Sidekiq::Stats and Redis
58
- queues.sort == all_queues.sort
59
- end
105
+ def call(*queues, skip_retries: false, skip_scheduled: false)
106
+ require "sidekiq/api"
60
107
 
61
- def fast_lookup(options)
62
- stats = ::Sidekiq::Stats.new
108
+ queues = normalize_queues(queues, allow_empty: true)
109
+ latencies = []
110
+ latencies << enqueued_latency(queues)
111
+ latencies << set_latency(::Sidekiq::RetrySet.new, queues) unless skip_retries
112
+ latencies << set_latency(::Sidekiq::ScheduledSet.new, queues) unless skip_scheduled
113
+ latencies.max
114
+ end
63
115
 
64
- in_queues = stats.enqueued
116
+ private
65
117
 
66
- if !options[:skip_scheduled]
67
- in_schedule = ::Sidekiq.redis { |c| c.zcount("schedule", "-inf", Time.now.to_f) }
118
+ def enqueued_latency(queues)
119
+ queues = registered_queues if queues.empty?
120
+
121
+ oldest_jobs = ::Sidekiq.redis do |conn|
122
+ conn.pipelined do |pipeline|
123
+ queues.each do |queue|
124
+ pipeline.lindex("queue:#{queue}", -1)
125
+ end
126
+ end
127
+ end
128
+
129
+ max_latencies = oldest_jobs.map do |job_payload|
130
+ job = job_payload ? JSON.parse(job_payload) : {}
131
+ job["enqueued_at"] ? Time.now.to_f - job["enqueued_at"] : 0.0
132
+ end
133
+
134
+ max_latencies.max || 0.0
68
135
  end
69
136
 
70
- if !options[:skip_retries]
71
- in_retry = ::Sidekiq.redis { |c| c.zcount("retry", "-inf", Time.now.to_f) }
137
+ def set_latency(set, queues)
138
+ max_latency = 0.0
139
+ now = Time.now
140
+
141
+ find_each_in_set(set) do |job|
142
+ if job.at > now
143
+ break
144
+ elsif queues.empty? || queues.include?(job.queue)
145
+ max_latency = now - job.at
146
+ break
147
+ end
148
+ end
149
+
150
+ max_latency
72
151
  end
152
+ end
153
+
154
+ # @!visibility private
155
+ module JobQueueSize
156
+ extend Common
157
+ extend HireFire::Utility
158
+ extend self
73
159
 
74
- if !options[:skip_working]
75
- in_progress = stats.workers_size
160
+ def call(*queues, server: false, **options)
161
+ require "sidekiq/api"
162
+
163
+ queues = normalize_queues(queues, allow_empty: true)
164
+
165
+ if server
166
+ server_lookup(queues, **options)
167
+ else
168
+ client_lookup(queues, **options)
169
+ end
76
170
  end
77
171
 
78
- [in_queues, in_schedule, in_retry, in_progress].compact.inject(&:+)
79
- end
172
+ private
80
173
 
81
- def dynamic_lookup(queues, options)
82
- in_queues = queues.inject(0) do |memo, name|
83
- memo += ::Sidekiq::Queue.new(name).size
84
- memo
174
+ def client_lookup(queues, skip_retries: false, skip_scheduled: false, skip_working: false, max_scheduled: nil)
175
+ size = enqueued_size(queues)
176
+ size += scheduled_size(queues, max_scheduled) unless skip_scheduled
177
+ size += retry_size(queues) unless skip_retries
178
+ size += working_size(queues) unless skip_working
179
+ size
85
180
  end
86
181
 
87
- if !options[:skip_scheduled]
88
- max = options[:max_scheduled]
182
+ def enqueued_size(queues)
183
+ queues = registered_queues if queues.empty?
89
184
 
90
- # For potentially long-running loops, compare all jobs against
91
- # time when the set snapshot was taken to avoid incorrect counts.
185
+ ::Sidekiq.redis do |conn|
186
+ conn.pipelined do |pipeline|
187
+ queues.each { |name| pipeline.llen("queue:#{name}") }
188
+ end
189
+ end.sum
190
+ end
191
+
192
+ def scheduled_size(queues, max = nil)
193
+ size, now = 0, Time.now
194
+
195
+ find_each_in_set(::Sidekiq::ScheduledSet.new) do |job|
196
+ if job.at > now || max && size >= max
197
+ break
198
+ elsif queues.empty? || queues.include?(job["queue"])
199
+ size += 1
200
+ end
201
+ end
202
+
203
+ size
204
+ end
205
+
206
+ def retry_size(queues)
207
+ size = 0
92
208
  now = Time.now
93
209
 
94
- in_schedule = ::Sidekiq::ScheduledSet.new.inject(0) do |memo, job|
95
- memo += 1 if queues.include?(job["queue"]) && job.at <= now
96
- break memo if max && memo >= max
97
- memo
210
+ find_each_in_set(::Sidekiq::RetrySet.new) do |job|
211
+ if job.at > now
212
+ break
213
+ elsif queues.empty? || queues.include?(job["queue"])
214
+ size += 1
215
+ end
98
216
  end
217
+
218
+ size
99
219
  end
100
220
 
101
- if !options[:skip_retries]
221
+ def working_size(queues)
102
222
  now = Time.now
223
+ now_as_i = now.to_i
103
224
 
104
- in_retry = ::Sidekiq::RetrySet.new.inject(0) do |memo, job|
105
- memo += 1 if queues.include?(job["queue"]) && job.at <= now
106
- memo
225
+ ::Sidekiq::Workers.new.count do |key, tid, job|
226
+ if Gem::Version.new(::Sidekiq::VERSION) >= Gem::Version.new("7.0.0")
227
+ (queues.empty? || queues.include?(job.queue)) && job.run_at <= now
228
+ else
229
+ (queues.empty? || queues.include?(job["queue"])) && job["run_at"] <= now_as_i
230
+ end
107
231
  end
108
232
  end
109
233
 
110
- now = Time.now.to_i
234
+ SERVER_SIDE_SCRIPT = <<~LUA
235
+ local tonumber = tonumber
236
+ local cjson_decode = cjson.decode
237
+
238
+ local function enqueued_size(queues)
239
+ local size = 0
111
240
 
112
- if !options[:skip_working]
113
- # Objects yielded to Workers#each:
114
- # https://github.com/mperham/sidekiq/blob/305ab8eedc362325da2e218b2a0e20e510668a42/lib/sidekiq/api.rb#L912
115
- in_progress = ::Sidekiq::Workers.new.count do |key, tid, job|
116
- queues.include?(job["queue"]) && job["run_at"] <= now
241
+ if next(queues) == nil then
242
+ queues = redis.call("keys", "queue:*")
243
+
244
+ for _, name in ipairs(queues) do
245
+ queues[string.sub(name, 7)] = true
246
+ end
247
+ end
248
+
249
+ for queue, _ in pairs(queues) do
250
+ size = size + redis.call("llen", "queue:" .. queue)
251
+ end
252
+
253
+ return size
254
+ end
255
+
256
+ local function set_size(queues, set, now, max)
257
+ local size = 0
258
+ local limit = 100
259
+ local offset = 0
260
+ local jobs
261
+
262
+ repeat
263
+ jobs = redis.call("zrangebyscore", set, "-inf", now, "LIMIT", offset, limit)
264
+ offset = offset + limit
265
+
266
+ for i = 1, #jobs do
267
+ local job = cjson_decode(jobs[i])
268
+
269
+ if job and (next(queues) == nil or queues[job.queue]) then
270
+ size = size + 1
271
+ end
272
+ end
273
+ until #jobs == 0 or (max > 0 and size >= max)
274
+
275
+ return size
276
+ end
277
+
278
+ local function working_size(queues)
279
+ local size = 0
280
+ local cursor = "0"
281
+
282
+ repeat
283
+ local process_sets = redis.call("SSCAN", "processes", cursor)
284
+ cursor = process_sets[1]
285
+
286
+ for _, process_key in ipairs(process_sets[2]) do
287
+ local worker_key = process_key .. ":work"
288
+ local worker_data = redis.call("HGETALL", worker_key)
289
+
290
+ for i = 2, #worker_data, 2 do
291
+ local worker = cjson_decode(worker_data[i])
292
+
293
+ if next(queues) == nil or queues[worker.queue] then
294
+ size = size + 1
295
+ end
296
+ end
297
+ end
298
+ until cursor == "0"
299
+
300
+ return size
301
+ end
302
+
303
+ local now = tonumber(ARGV[1])
304
+ local max_scheduled = tonumber(ARGV[2])
305
+ local skip_scheduled = tonumber(ARGV[3]) == 1
306
+ local skip_retries = tonumber(ARGV[4]) == 1
307
+ local skip_working = tonumber(ARGV[5]) == 1
308
+
309
+ local queues = {}
310
+ for i = 6, #ARGV do
311
+ queues[ARGV[i]] = true
312
+ end
313
+
314
+ local size = enqueued_size(queues)
315
+
316
+ if not skip_scheduled then
317
+ size = size + set_size(queues, "schedule", now, max_scheduled)
318
+ end
319
+
320
+ if not skip_retries then
321
+ size = size + set_size(queues, "retry", now, 0)
322
+ end
323
+
324
+ if not skip_working then
325
+ size = size + working_size(queues)
326
+ end
327
+
328
+ return size
329
+ LUA
330
+
331
+ SERVER_SIDE_SCRIPT_SHA = Digest::SHA1.hexdigest(SERVER_SIDE_SCRIPT).freeze
332
+
333
+ def server_lookup(queues, skip_scheduled: false, skip_retries: false, skip_working: false, max_scheduled: 0)
334
+ ::Sidekiq.redis do |connection|
335
+ now = Time.now.to_i
336
+ skip_scheduled = skip_scheduled ? 1 : 0
337
+ skip_retries = skip_retries ? 1 : 0
338
+ skip_working = skip_working ? 1 : 0
339
+
340
+ if defined?(::Sidekiq::RedisClientAdapter::CompatClient) && connection.is_a?(::Sidekiq::RedisClientAdapter::CompatClient)
341
+ count_with_redis_client(connection, now, max_scheduled, skip_scheduled, skip_retries, skip_working, *queues)
342
+ elsif defined?(::Redis) && connection.is_a?(::Redis)
343
+ count_with_redis(connection, now, max_scheduled, skip_scheduled, skip_retries, skip_working, *queues)
344
+ else
345
+ raise "Unsupported Redis connection type: #{connection.class}"
346
+ end
117
347
  end
118
348
  end
119
349
 
120
- [in_queues, in_schedule, in_retry, in_progress].compact.inject(&:+)
350
+ def count_with_redis(connection, *args)
351
+ connection.evalsha(SERVER_SIDE_SCRIPT_SHA, argv: args)
352
+ rescue Redis::CommandError => e
353
+ if e.message.include?("NOSCRIPT")
354
+ connection.script(:load, SERVER_SIDE_SCRIPT)
355
+ retry
356
+ else
357
+ raise
358
+ end
359
+ end
360
+
361
+ def count_with_redis_client(connection, *args)
362
+ connection.call("evalsha", SERVER_SIDE_SCRIPT_SHA, 0, *args)
363
+ rescue RedisClient::CommandError => e
364
+ if e.message.include?("NOSCRIPT")
365
+ connection.call("script", "load", SERVER_SIDE_SCRIPT)
366
+ retry
367
+ else
368
+ raise
369
+ end
370
+ end
121
371
  end
122
372
  end
123
373
  end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HireFire
4
+ module Macro
5
+ module SolidQueue
6
+ extend HireFire::Utility
7
+ extend self
8
+
9
+ LATENCY_METHODS = [
10
+ :ready_latency,
11
+ :scheduled_latency,
12
+ :blocked_latency
13
+ ].freeze
14
+
15
+ # Calculates the maximum job queue latency using SolidQueue. If no queues are specified, it
16
+ # measures latency across all available queues.
17
+ #
18
+ # This function measures the job queue latency across the Ready, Scheduled, and Blocked
19
+ # queues, based on the enqueue, schedule, and expiration times of their executions. Executions
20
+ # in the Claimed queue, as well as in paused queues, are excluded from the calculation.
21
+ #
22
+ # @param queues [Array<String, Symbol>] (optional) Names of the queues for latency
23
+ # measurement. If not provided, latency is measured across all queues.
24
+ # @return [Float] Maximum job queue latency in seconds.
25
+ # @example Calculate latency across all queues
26
+ # HireFire::Macro::SolidQueue.job_queue_latency
27
+ # @example Calculate latency for the "default" queue
28
+ # HireFire::Macro::SolidQueue.job_queue_latency(:default)
29
+ # @example Calculate latency across "default" and "mailer" queues
30
+ # HireFire::Macro::SolidQueue.job_queue_latency(:default, :mailer)
31
+ # @example Calculate latency across "mailer_*" queues (i.e. mailer_notification, mailer_newsletter)
32
+ # HireFire::Macro::SolidQueue.job_queue_latency(:"mailer_*")
33
+ def job_queue_latency(*queues)
34
+ queues, now = determine_queues(queues), Time.now
35
+
36
+ LATENCY_METHODS.map do |latency_method|
37
+ method(latency_method).call(queues, now: now)
38
+ end.max
39
+ end
40
+
41
+ SIZE_METHODS = [
42
+ :ready_size,
43
+ :scheduled_size,
44
+ :claimed_size,
45
+ :blocked_size
46
+ ].freeze
47
+
48
+ # Calculates the total job queue size using SolidQueue. If no queues are specified, it
49
+ # measures size across all available queues.
50
+ #
51
+ # This function measures the job queue latency across the Ready, Scheduled, Blocked, and
52
+ # Claimed queues, based on the schedule and expiration times of their executions. Executions
53
+ # in paused queues are excluded from the calculation.
54
+ #
55
+ # @param queues [Array<String, Symbol>] (optional) Names of the queues for size measurement.
56
+ # If not provided, size is measured across all queues.
57
+ # @return [Integer] Total job queue size.
58
+ # @example Calculate size across all queues
59
+ # HireFire::Macro::SolidQueue.job_queue_size
60
+ # @example Calculate size for the "default" queue
61
+ # HireFire::Macro::SolidQueue.job_queue_size(:default)
62
+ # @example Calculate size across "default" and "mailer" queues
63
+ # HireFire::Macro::SolidQueue.job_queue_size(:default, :mailer)
64
+ # @example Calculate size across "mailer_*" queues (i.e. mailer_notification, mailer_newsletter)
65
+ # HireFire::Macro::SolidQueue.job_queue_size(:"mailer_*")
66
+ def job_queue_size(*queues)
67
+ queues = determine_queues(queues)
68
+
69
+ SIZE_METHODS.sum do |count_method|
70
+ method(count_method).call(queues)
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def determine_queues(queues)
77
+ queues = normalize_queues(queues, allow_empty: true)
78
+
79
+ Set.new(
80
+ if queues.empty?
81
+ registered_queues
82
+ elsif queues.any? { |queue| queue.end_with?("*") }
83
+ expand_wildcards(queues)
84
+ else
85
+ queues
86
+ end
87
+ ) - paused_queues
88
+ end
89
+
90
+ def registered_queues
91
+ ::SolidQueue::Queue.all.map(&:name)
92
+ end
93
+
94
+ def paused_queues
95
+ ::SolidQueue::Pause.pluck(:queue_name)
96
+ end
97
+
98
+ def expand_wildcards(queues)
99
+ cached_registered_queues = registered_queues
100
+
101
+ queues.flat_map do |queue|
102
+ if queue.end_with?("*")
103
+ cached_registered_queues.select do |registered_queue|
104
+ registered_queue.start_with?(queue[0..-2])
105
+ end
106
+ else
107
+ queue
108
+ end
109
+ end
110
+ end
111
+
112
+ def ready_latency(queues, now:)
113
+ now - (
114
+ ::SolidQueue::ReadyExecution
115
+ .where(queue_name: queues)
116
+ .minimum(:created_at) || now
117
+ )
118
+ end
119
+
120
+ def ready_size(queues)
121
+ ::SolidQueue::ReadyExecution
122
+ .where(queue_name: queues)
123
+ .count
124
+ end
125
+
126
+ def scheduled_latency(queues, now:)
127
+ now - (
128
+ ::SolidQueue::ScheduledExecution
129
+ .due
130
+ .where(queue_name: queues)
131
+ .minimum(:scheduled_at) || now
132
+ )
133
+ end
134
+
135
+ def scheduled_size(queues)
136
+ ::SolidQueue::ScheduledExecution
137
+ .due
138
+ .where(queue_name: queues)
139
+ .count
140
+ end
141
+
142
+ def blocked_latency(queues, now:)
143
+ now - (
144
+ ::SolidQueue::BlockedExecution
145
+ .expired
146
+ .where(queue_name: queues)
147
+ .minimum(:expires_at) || now
148
+ )
149
+ end
150
+
151
+ def blocked_size(queues)
152
+ ::SolidQueue::BlockedExecution
153
+ .expired
154
+ .where(queue_name: queues)
155
+ .count
156
+ end
157
+
158
+ def claimed_size(queues)
159
+ ::SolidQueue::Job
160
+ .joins(:claimed_execution)
161
+ .where(queue_name: queues)
162
+ .count
163
+ end
164
+ end
165
+ end
166
+ end