good_job 4.1.0 → 4.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/app/charts/good_job/performance_index_chart.rb +1 -1
- data/app/charts/good_job/performance_show_chart.rb +1 -1
- data/app/controllers/good_job/application_controller.rb +1 -1
- data/app/controllers/good_job/frontends_controller.rb +6 -2
- data/app/controllers/good_job/performance_controller.rb +1 -1
- data/app/frontend/good_job/icons.svg +79 -0
- data/app/frontend/good_job/style.css +5 -0
- data/app/helpers/good_job/icons_helper.rb +8 -5
- data/app/models/concerns/good_job/advisory_lockable.rb +17 -7
- data/app/models/concerns/good_job/reportable.rb +8 -12
- data/app/models/good_job/batch.rb +10 -5
- data/app/models/good_job/batch_record.rb +18 -15
- data/app/models/good_job/discrete_execution.rb +6 -59
- data/app/models/good_job/execution.rb +59 -4
- data/app/models/good_job/execution_result.rb +6 -6
- data/app/models/good_job/job.rb +567 -12
- data/app/views/good_job/batches/_jobs.erb +1 -1
- data/app/views/good_job/batches/_table.erb +1 -1
- data/app/views/good_job/jobs/index.html.erb +1 -1
- data/app/views/layouts/good_job/application.html.erb +7 -7
- data/config/brakeman.ignore +75 -0
- data/config/locales/de.yml +49 -49
- data/config/locales/es.yml +14 -14
- data/config/routes.rb +3 -3
- data/lib/good_job/active_job_extensions/concurrency.rb +105 -98
- data/lib/good_job/adapter/inline_buffer.rb +73 -0
- data/lib/good_job/adapter.rb +59 -53
- data/lib/good_job/configuration.rb +3 -4
- data/lib/good_job/cron_manager.rb +1 -3
- data/lib/good_job/current_thread.rb +4 -4
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +6 -5
- metadata +6 -20
- data/app/models/good_job/base_execution.rb +0 -605
- data/app/views/good_job/shared/icons/_arrow_clockwise.html.erb +0 -5
- data/app/views/good_job/shared/icons/_check.html.erb +0 -5
- data/app/views/good_job/shared/icons/_circle_half.html.erb +0 -4
- data/app/views/good_job/shared/icons/_clock.html.erb +0 -5
- data/app/views/good_job/shared/icons/_dash_circle.html.erb +0 -5
- data/app/views/good_job/shared/icons/_dots.html.erb +0 -3
- data/app/views/good_job/shared/icons/_eject.html.erb +0 -4
- data/app/views/good_job/shared/icons/_exclamation.html.erb +0 -5
- data/app/views/good_job/shared/icons/_globe.html.erb +0 -3
- data/app/views/good_job/shared/icons/_info.html.erb +0 -4
- data/app/views/good_job/shared/icons/_moon_stars_fill.html.erb +0 -5
- data/app/views/good_job/shared/icons/_pause.html.erb +0 -4
- data/app/views/good_job/shared/icons/_play.html.erb +0 -4
- data/app/views/good_job/shared/icons/_skip_forward.html.erb +0 -4
- data/app/views/good_job/shared/icons/_stop.html.erb +0 -4
- data/app/views/good_job/shared/icons/_sun_fill.html.erb +0 -4
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module/attribute_accessors_per_thread'
|
4
|
+
|
5
|
+
module GoodJob
|
6
|
+
class Adapter
|
7
|
+
# The InlineBuffer is integrated into the Adapter and captures jobs that have been enqueued inline.
|
8
|
+
# The purpose is allow job records to be persisted, in a locked state, while within a transaction,
|
9
|
+
# and then execute the jobs after the transaction has been committed to ensure that the jobs
|
10
|
+
# do not run within a transaction.
|
11
|
+
#
|
12
|
+
# @private This is intended for internal GoodJob usage only.
|
13
|
+
class InlineBuffer
|
14
|
+
# @!attribute [rw] current_buffer
|
15
|
+
# @!scope class
|
16
|
+
# Current buffer of jobs to be enqueued.
|
17
|
+
# @return [GoodJob::Adapter::InlineBuffer, nil]
|
18
|
+
thread_mattr_accessor :current_buffer
|
19
|
+
|
20
|
+
# This block should be used to wrap the transaction that could enqueue jobs.
|
21
|
+
# @yield The block that may enqueue jobs.
|
22
|
+
# @return [Proc] A proc that will execute enqueued jobs after the transaction has been committed.
|
23
|
+
# @example Wrapping a transaction
|
24
|
+
# buffer = GoodJob::Adapter::InlineBuffer.capture do
|
25
|
+
# ActiveRecord::Base.transaction do
|
26
|
+
# MyJob.perform_later
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
# buffer.call
|
30
|
+
def self.capture
|
31
|
+
if current_buffer
|
32
|
+
yield
|
33
|
+
return proc {}
|
34
|
+
end
|
35
|
+
|
36
|
+
begin
|
37
|
+
self.current_buffer = new
|
38
|
+
yield
|
39
|
+
current_buffer.to_proc
|
40
|
+
ensure
|
41
|
+
self.current_buffer = nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Used within the adapter to wrap inline job execution
|
46
|
+
def self.perform_now_or_defer(&block)
|
47
|
+
if defer?
|
48
|
+
current_buffer.defer(block)
|
49
|
+
else
|
50
|
+
yield
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.defer?
|
55
|
+
current_buffer.present?
|
56
|
+
end
|
57
|
+
|
58
|
+
def initialize
|
59
|
+
@callables = []
|
60
|
+
end
|
61
|
+
|
62
|
+
def defer(callable)
|
63
|
+
@callables << callable
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_proc
|
67
|
+
proc do
|
68
|
+
@callables.map(&:call)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/good_job/adapter.rb
CHANGED
@@ -57,7 +57,7 @@ module GoodJob
|
|
57
57
|
|
58
58
|
Rails.application.executor.wrap do
|
59
59
|
current_time = Time.current
|
60
|
-
|
60
|
+
jobs = active_jobs.map do |active_job|
|
61
61
|
GoodJob::Job.build_for_enqueue(active_job).tap do |job|
|
62
62
|
job.scheduled_at = current_time if job.scheduled_at == job.created_at
|
63
63
|
job.created_at = current_time
|
@@ -65,62 +65,56 @@ module GoodJob
|
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
68
|
-
|
68
|
+
inline_jobs = []
|
69
69
|
GoodJob::Job.transaction(requires_new: true, joinable: false) do
|
70
|
-
|
71
|
-
results = GoodJob::Job.insert_all(
|
70
|
+
job_attributes = jobs.map(&:attributes)
|
71
|
+
results = GoodJob::Job.insert_all(job_attributes, returning: %w[id active_job_id]) # rubocop:disable Rails/SkipsModelValidations
|
72
72
|
|
73
73
|
job_id_to_provider_job_id = results.each_with_object({}) { |result, hash| hash[result['active_job_id']] = result['id'] }
|
74
74
|
active_jobs.each do |active_job|
|
75
75
|
active_job.provider_job_id = job_id_to_provider_job_id[active_job.job_id]
|
76
76
|
active_job.successfully_enqueued = active_job.provider_job_id.present? if active_job.respond_to?(:successfully_enqueued=)
|
77
77
|
end
|
78
|
-
|
79
|
-
|
78
|
+
jobs.each do |job|
|
79
|
+
job.instance_variable_set(:@new_record, false) if job_id_to_provider_job_id[job.active_job_id]
|
80
80
|
end
|
81
|
-
|
81
|
+
jobs = jobs.select(&:persisted?) # prune unpersisted jobs
|
82
82
|
|
83
83
|
if execute_inline?
|
84
|
-
|
85
|
-
|
84
|
+
inline_jobs = jobs.select { |job| job.scheduled_at.nil? || job.scheduled_at <= Time.current }
|
85
|
+
inline_jobs.each(&:advisory_lock!)
|
86
86
|
end
|
87
87
|
end
|
88
88
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
retried_execution = inline_result.retried
|
97
|
-
while retried_execution && retried_execution.scheduled_at <= Time.current
|
98
|
-
inline_execution = retried_execution
|
99
|
-
inline_result = inline_execution.perform(lock_id: @capsule.tracker.id_for_lock)
|
100
|
-
retried_execution = inline_result.retried
|
89
|
+
if inline_jobs.any?
|
90
|
+
deferred = InlineBuffer.defer?
|
91
|
+
InlineBuffer.perform_now_or_defer do
|
92
|
+
@capsule.tracker.register do
|
93
|
+
until inline_jobs.empty?
|
94
|
+
inline_job = inline_jobs.shift
|
95
|
+
perform_inline(inline_job, notify: deferred ? send_notify?(inline_job) : false)
|
101
96
|
end
|
102
97
|
ensure
|
103
|
-
|
104
|
-
inline_execution.run_callbacks(:perform_unlocked)
|
98
|
+
inline_jobs.each(&:advisory_unlock)
|
105
99
|
end
|
106
|
-
raise inline_result.unhandled_error if inline_result.unhandled_error
|
107
100
|
end
|
108
|
-
ensure
|
109
|
-
@capsule.tracker.unregister
|
110
|
-
inline_executions.each(&:advisory_unlock)
|
111
101
|
end
|
112
102
|
|
113
|
-
|
114
|
-
|
103
|
+
non_inline_jobs = if InlineBuffer.defer?
|
104
|
+
jobs - inline_jobs
|
105
|
+
else
|
106
|
+
jobs.reject(&:finished_at)
|
107
|
+
end
|
108
|
+
if non_inline_jobs.any?
|
115
109
|
job_id_to_active_jobs = active_jobs.index_by(&:job_id)
|
116
|
-
|
117
|
-
|
118
|
-
state = { queue_name: queue_name, count:
|
110
|
+
non_inline_jobs.group_by(&:queue_name).each do |queue_name, jobs_by_queue|
|
111
|
+
jobs_by_queue.group_by(&:scheduled_at).each do |scheduled_at, jobs_by_queue_and_scheduled_at|
|
112
|
+
state = { queue_name: queue_name, count: jobs_by_queue_and_scheduled_at.size }
|
119
113
|
state[:scheduled_at] = scheduled_at if scheduled_at
|
120
114
|
|
121
115
|
executed_locally = execute_async? && @capsule&.create_thread(state)
|
122
116
|
unless executed_locally
|
123
|
-
state[:count] = job_id_to_active_jobs.values_at(*
|
117
|
+
state[:count] = job_id_to_active_jobs.values_at(*jobs_by_queue_and_scheduled_at.map(&:active_job_id)).count { |active_job| send_notify?(active_job) }
|
124
118
|
Notifier.notify(state) unless state[:count].zero?
|
125
119
|
end
|
126
120
|
end
|
@@ -148,43 +142,32 @@ module GoodJob
|
|
148
142
|
will_retry_inline = will_execute_inline && CurrentThread.job&.active_job_id == active_job.job_id && !CurrentThread.retry_now
|
149
143
|
|
150
144
|
if will_retry_inline
|
151
|
-
|
145
|
+
job = GoodJob::Job.enqueue(
|
152
146
|
active_job,
|
153
147
|
scheduled_at: scheduled_at
|
154
148
|
)
|
155
149
|
elsif will_execute_inline
|
156
|
-
|
150
|
+
job = GoodJob::Job.enqueue(
|
157
151
|
active_job,
|
158
152
|
scheduled_at: scheduled_at,
|
159
153
|
create_with_advisory_lock: true
|
160
154
|
)
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
retried_execution = result.retried
|
165
|
-
while retried_execution && (retried_execution.scheduled_at.nil? || retried_execution.scheduled_at <= Time.current)
|
166
|
-
execution = retried_execution
|
167
|
-
result = @capsule.tracker.register { execution.perform(lock_id: @capsule.tracker.id_for_lock) }
|
168
|
-
retried_execution = result.retried
|
155
|
+
InlineBuffer.perform_now_or_defer do
|
156
|
+
@capsule.tracker.register do
|
157
|
+
perform_inline(job, notify: send_notify?(active_job))
|
169
158
|
end
|
170
|
-
|
171
|
-
Notifier.notify(retried_execution.job_state) if retried_execution&.scheduled_at && retried_execution.scheduled_at > Time.current && send_notify?(active_job)
|
172
|
-
ensure
|
173
|
-
execution.advisory_unlock
|
174
|
-
execution.run_callbacks(:perform_unlocked)
|
175
159
|
end
|
176
|
-
raise result.unhandled_error if result.unhandled_error
|
177
160
|
else
|
178
|
-
|
161
|
+
job = GoodJob::Job.enqueue(
|
179
162
|
active_job,
|
180
163
|
scheduled_at: scheduled_at
|
181
164
|
)
|
182
165
|
|
183
|
-
executed_locally = execute_async? && @capsule&.create_thread(
|
184
|
-
Notifier.notify(
|
166
|
+
executed_locally = execute_async? && @capsule&.create_thread(job.job_state)
|
167
|
+
Notifier.notify(job.job_state) if !executed_locally && send_notify?(active_job)
|
185
168
|
end
|
186
169
|
|
187
|
-
|
170
|
+
job
|
188
171
|
end
|
189
172
|
end
|
190
173
|
|
@@ -250,5 +233,28 @@ module GoodJob
|
|
250
233
|
|
251
234
|
!(active_job.good_job_notify == false || (active_job.class.good_job_notify == false && active_job.good_job_notify.nil?))
|
252
235
|
end
|
236
|
+
|
237
|
+
# @param job [GoodJob::Job] the job to perform, which must be enqueued and advisory locked already
|
238
|
+
# @param notify [Boolean] whether to send a NOTIFY event for a retried job
|
239
|
+
def perform_inline(job, notify: true)
|
240
|
+
result = nil
|
241
|
+
retried_job = nil
|
242
|
+
|
243
|
+
loop do
|
244
|
+
result = job.perform(lock_id: @capsule.tracker.id_for_lock)
|
245
|
+
retried_job = result.retried_job
|
246
|
+
break if retried_job.nil? || retried_job.scheduled_at.nil? || retried_job.scheduled_at > Time.current
|
247
|
+
|
248
|
+
job = retried_job
|
249
|
+
end
|
250
|
+
|
251
|
+
Notifier.notify(retried_job.job_state) if notify && retried_job&.scheduled_at && retried_job.scheduled_at > Time.current
|
252
|
+
result
|
253
|
+
ensure
|
254
|
+
job.advisory_unlock
|
255
|
+
job.run_callbacks(:perform_unlocked)
|
256
|
+
|
257
|
+
raise result.unhandled_error if result.unhandled_error
|
258
|
+
end
|
253
259
|
end
|
254
260
|
end
|
@@ -150,11 +150,10 @@ module GoodJob
|
|
150
150
|
# poll (using this interval) for new queued jobs to execute.
|
151
151
|
# @return [Integer]
|
152
152
|
def poll_interval
|
153
|
-
interval =
|
153
|
+
interval =
|
154
154
|
options[:poll_interval] ||
|
155
|
-
|
156
|
-
|
157
|
-
)
|
155
|
+
rails_config[:poll_interval] ||
|
156
|
+
env['GOOD_JOB_POLL_INTERVAL']
|
158
157
|
|
159
158
|
if interval
|
160
159
|
interval.to_i
|
@@ -55,9 +55,7 @@ module GoodJob # :nodoc:
|
|
55
55
|
# @param timeout [Numeric, nil] Unused but retained for compatibility
|
56
56
|
def shutdown(timeout: nil) # rubocop:disable Lint/UnusedMethodArgument
|
57
57
|
@running = false
|
58
|
-
@tasks.
|
59
|
-
task.cancel
|
60
|
-
end
|
58
|
+
@tasks.each_value(&:cancel)
|
61
59
|
@tasks.clear
|
62
60
|
end
|
63
61
|
|
@@ -15,7 +15,7 @@ module GoodJob
|
|
15
15
|
error_on_retry_stopped
|
16
16
|
job
|
17
17
|
execution_interrupted
|
18
|
-
|
18
|
+
retried_job
|
19
19
|
retry_now
|
20
20
|
].freeze
|
21
21
|
|
@@ -61,11 +61,11 @@ module GoodJob
|
|
61
61
|
# @return [Boolean, nil]
|
62
62
|
thread_mattr_accessor :execution_interrupted
|
63
63
|
|
64
|
-
# @!attribute [rw]
|
64
|
+
# @!attribute [rw] retried_job
|
65
65
|
# @!scope class
|
66
66
|
# Execution Retried
|
67
|
-
# @return [
|
68
|
-
thread_mattr_accessor :
|
67
|
+
# @return [GoodJob::Job, nil]
|
68
|
+
thread_mattr_accessor :retried_job
|
69
69
|
|
70
70
|
# @!attribute [rw] retry_now
|
71
71
|
# @!scope class
|
data/lib/good_job/version.rb
CHANGED
data/lib/good_job.rb
CHANGED
@@ -7,6 +7,7 @@ require_relative "good_job/version"
|
|
7
7
|
require_relative "good_job/engine"
|
8
8
|
|
9
9
|
require_relative "good_job/adapter"
|
10
|
+
require_relative "good_job/adapter/inline_buffer"
|
10
11
|
require_relative "active_job/queue_adapters/good_job_adapter"
|
11
12
|
require_relative "good_job/active_job_extensions/batches"
|
12
13
|
require_relative "good_job/active_job_extensions/concurrency"
|
@@ -211,7 +212,7 @@ module GoodJob
|
|
211
212
|
ActiveSupport::Notifications.instrument("cleanup_preserved_jobs.good_job", { older_than: older_than, timestamp: timestamp }) do |payload|
|
212
213
|
deleted_jobs_count = 0
|
213
214
|
deleted_batches_count = 0
|
214
|
-
|
215
|
+
deleted_executions_count = 0
|
215
216
|
|
216
217
|
jobs_query = GoodJob::Job.finished_before(timestamp).order(finished_at: :asc).limit(in_batches_of)
|
217
218
|
jobs_query = jobs_query.succeeded unless include_discarded
|
@@ -219,8 +220,8 @@ module GoodJob
|
|
219
220
|
active_job_ids = jobs_query.pluck(:active_job_id)
|
220
221
|
break if active_job_ids.empty?
|
221
222
|
|
222
|
-
|
223
|
-
|
223
|
+
deleted_executions = GoodJob::Execution.where(active_job_id: active_job_ids).delete_all
|
224
|
+
deleted_executions_count += deleted_executions
|
224
225
|
|
225
226
|
deleted_jobs = GoodJob::Job.where(active_job_id: active_job_ids).delete_all
|
226
227
|
deleted_jobs_count += deleted_jobs
|
@@ -236,10 +237,10 @@ module GoodJob
|
|
236
237
|
end
|
237
238
|
|
238
239
|
payload[:destroyed_batches_count] = deleted_batches_count
|
239
|
-
payload[:
|
240
|
+
payload[:destroyed_executions_count] = deleted_executions_count
|
240
241
|
payload[:destroyed_jobs_count] = deleted_jobs_count
|
241
242
|
|
242
|
-
destroyed_records_count = deleted_batches_count +
|
243
|
+
destroyed_records_count = deleted_batches_count + deleted_executions_count + deleted_jobs_count
|
243
244
|
payload[:destroyed_records_count] = destroyed_records_count
|
244
245
|
|
245
246
|
destroyed_records_count
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: good_job
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.1.
|
4
|
+
version: 4.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Sheldon
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-07-
|
11
|
+
date: 2024-07-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -264,6 +264,7 @@ files:
|
|
264
264
|
- app/filters/good_job/batches_filter.rb
|
265
265
|
- app/filters/good_job/jobs_filter.rb
|
266
266
|
- app/frontend/good_job/application.js
|
267
|
+
- app/frontend/good_job/icons.svg
|
267
268
|
- app/frontend/good_job/modules/async_values_controller.js
|
268
269
|
- app/frontend/good_job/modules/charts.js
|
269
270
|
- app/frontend/good_job/modules/checkbox_toggle.js
|
@@ -286,7 +287,6 @@ files:
|
|
286
287
|
- app/models/concerns/good_job/filterable.rb
|
287
288
|
- app/models/concerns/good_job/reportable.rb
|
288
289
|
- app/models/good_job/active_record_parent_class.rb
|
289
|
-
- app/models/good_job/base_execution.rb
|
290
290
|
- app/models/good_job/base_record.rb
|
291
291
|
- app/models/good_job/batch.rb
|
292
292
|
- app/models/good_job/batch_record.rb
|
@@ -319,24 +319,9 @@ files:
|
|
319
319
|
- app/views/good_job/shared/_footer.erb
|
320
320
|
- app/views/good_job/shared/_navbar.erb
|
321
321
|
- app/views/good_job/shared/_secondary_navbar.erb
|
322
|
-
- app/views/good_job/shared/icons/_arrow_clockwise.html.erb
|
323
|
-
- app/views/good_job/shared/icons/_check.html.erb
|
324
|
-
- app/views/good_job/shared/icons/_circle_half.html.erb
|
325
|
-
- app/views/good_job/shared/icons/_clock.html.erb
|
326
|
-
- app/views/good_job/shared/icons/_dash_circle.html.erb
|
327
|
-
- app/views/good_job/shared/icons/_dots.html.erb
|
328
|
-
- app/views/good_job/shared/icons/_eject.html.erb
|
329
|
-
- app/views/good_job/shared/icons/_exclamation.html.erb
|
330
|
-
- app/views/good_job/shared/icons/_globe.html.erb
|
331
|
-
- app/views/good_job/shared/icons/_info.html.erb
|
332
|
-
- app/views/good_job/shared/icons/_moon_stars_fill.html.erb
|
333
|
-
- app/views/good_job/shared/icons/_pause.html.erb
|
334
|
-
- app/views/good_job/shared/icons/_play.html.erb
|
335
|
-
- app/views/good_job/shared/icons/_skip_forward.html.erb
|
336
|
-
- app/views/good_job/shared/icons/_stop.html.erb
|
337
|
-
- app/views/good_job/shared/icons/_sun_fill.html.erb
|
338
322
|
- app/views/good_job/shared/icons/_trash.html.erb
|
339
323
|
- app/views/layouts/good_job/application.html.erb
|
324
|
+
- config/brakeman.ignore
|
340
325
|
- config/locales/de.yml
|
341
326
|
- config/locales/en.yml
|
342
327
|
- config/locales/es.yml
|
@@ -363,6 +348,7 @@ files:
|
|
363
348
|
- lib/good_job/active_job_extensions/labels.rb
|
364
349
|
- lib/good_job/active_job_extensions/notify_options.rb
|
365
350
|
- lib/good_job/adapter.rb
|
351
|
+
- lib/good_job/adapter/inline_buffer.rb
|
366
352
|
- lib/good_job/bulk.rb
|
367
353
|
- lib/good_job/callable.rb
|
368
354
|
- lib/good_job/capsule.rb
|
@@ -427,7 +413,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
427
413
|
- !ruby/object:Gem::Version
|
428
414
|
version: '0'
|
429
415
|
requirements: []
|
430
|
-
rubygems_version: 3.5.
|
416
|
+
rubygems_version: 3.5.11
|
431
417
|
signing_key:
|
432
418
|
specification_version: 4
|
433
419
|
summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails
|