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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +175 -0
- data/LICENSE +21 -66
- data/README.md +34 -78
- data/hirefire-resource.gemspec +21 -15
- data/lib/hirefire/configuration.rb +31 -0
- data/lib/hirefire/errors/job_queue_latency_unsupported.rb +12 -0
- data/lib/hirefire/errors.rb +9 -0
- data/lib/hirefire/hirefire.rb +15 -0
- data/lib/hirefire/macro/bunny.rb +55 -55
- data/lib/hirefire/macro/delayed_job.rb +80 -60
- data/lib/hirefire/macro/deprecated/bunny.rb +85 -0
- data/lib/hirefire/macro/deprecated/delayed_job.rb +62 -0
- data/lib/hirefire/macro/deprecated/good_job.rb +32 -0
- data/lib/hirefire/macro/deprecated/que.rb +76 -0
- data/lib/hirefire/macro/deprecated/queue_classic.rb +28 -0
- data/lib/hirefire/macro/deprecated/resque.rb +47 -0
- data/lib/hirefire/macro/deprecated/sidekiq.rb +138 -0
- data/lib/hirefire/macro/good_job.rb +48 -13
- data/lib/hirefire/macro/que.rb +56 -36
- data/lib/hirefire/macro/queue_classic.rb +86 -0
- data/lib/hirefire/macro/resque.rb +112 -24
- data/lib/hirefire/macro/sidekiq.rb +324 -74
- data/lib/hirefire/macro/solid_queue.rb +166 -0
- data/lib/hirefire/middleware.rb +50 -103
- data/lib/hirefire/railtie.rb +1 -1
- data/lib/hirefire/resource.rb +1 -47
- data/lib/hirefire/utility.rb +22 -0
- data/lib/hirefire/version.rb +5 -0
- data/lib/hirefire/web.rb +151 -0
- data/lib/hirefire/worker.rb +39 -0
- data/lib/hirefire-resource.rb +4 -9
- metadata +50 -24
- data/.github/workflows/main.yml +0 -16
- data/.gitignore +0 -5
- data/Gemfile +0 -10
- data/Gemfile.lock +0 -49
- data/Rakefile +0 -4
- data/lib/hirefire/macro/qc.rb +0 -23
- 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
|
-
#
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
#
|
|
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]
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
67
|
+
# @!visibility private
|
|
68
|
+
module Common
|
|
69
|
+
private
|
|
35
70
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
86
|
+
entries.each do |entry, score|
|
|
87
|
+
yield ::Sidekiq::SortedEntry.new(self, score, entry)
|
|
88
|
+
end
|
|
45
89
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
116
|
+
private
|
|
65
117
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
end
|
|
172
|
+
private
|
|
80
173
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
182
|
+
def enqueued_size(queues)
|
|
183
|
+
queues = registered_queues if queues.empty?
|
|
89
184
|
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
221
|
+
def working_size(queues)
|
|
102
222
|
now = Time.now
|
|
223
|
+
now_as_i = now.to_i
|
|
103
224
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|