good_job 1.10.1 → 1.11.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 +16 -0
- data/README.md +30 -0
- data/lib/good_job/active_job_extensions.rb +4 -0
- data/lib/good_job/active_job_extensions/concurrency.rb +68 -0
- data/lib/good_job/current_execution.rb +12 -5
- data/lib/good_job/job.rb +20 -0
- data/lib/good_job/lockable.rb +10 -3
- data/lib/good_job/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6c0b45fcd80fbd536af0f6fed44ef53f446ee6574f7e90d41a38585ec8b3ba63
|
4
|
+
data.tar.gz: 8b45f77d94bd809b25fb725f57622f1966f188157f235e03c56442817e31afd4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 820a1847c2dfce6456d925fe73e8373d5b2f19ff9bc2dd143dbf942acfb0c3872b4fdd1dc888741ae7f3974c7f6869ef82ef8c54bd238ceea03bb63ff8829593
|
7
|
+
data.tar.gz: 4faa2442654f37475c22ce7c3e3b440351a5f91a53ac2ada273cba764c409e7f7f108c26b463a736b9880d4097ef9096775905c092bf5860805e3cf7bffede86
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,21 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v1.11.0](https://github.com/bensheldon/good_job/tree/v1.11.0) (2021-07-07)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.10.1...v1.11.0)
|
6
|
+
|
7
|
+
**Implemented enhancements:**
|
8
|
+
|
9
|
+
- Add concurrency extension for ActiveJob [\#281](https://github.com/bensheldon/good_job/pull/281) ([bensheldon](https://github.com/bensheldon))
|
10
|
+
|
11
|
+
**Closed issues:**
|
12
|
+
|
13
|
+
- Investigate GoodJob concurrency [\#289](https://github.com/bensheldon/good_job/issues/289)
|
14
|
+
- Problem with migrating database on 1.10.0 [\#287](https://github.com/bensheldon/good_job/issues/287)
|
15
|
+
- Support migration --database option for install task? [\#267](https://github.com/bensheldon/good_job/issues/267)
|
16
|
+
- Add GoodJob to Ruby Toolbox [\#243](https://github.com/bensheldon/good_job/issues/243)
|
17
|
+
- Custom advisory locks to prevent certain jobs from being worked on concurrently? [\#206](https://github.com/bensheldon/good_job/issues/206)
|
18
|
+
|
3
19
|
## [v1.10.1](https://github.com/bensheldon/good_job/tree/v1.10.1) (2021-06-30)
|
4
20
|
|
5
21
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v1.10.0...v1.10.1)
|
data/README.md
CHANGED
@@ -38,6 +38,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
|
|
38
38
|
- [Configuration options](#configuration-options)
|
39
39
|
- [Global options](#global-options)
|
40
40
|
- [Dashboard](#dashboard)
|
41
|
+
- [ActiveJob Concurrency](#activejob-concurrency)
|
41
42
|
- [Updating](#updating)
|
42
43
|
- [Go deeper](#go-deeper)
|
43
44
|
- [Exceptions, retries, and reliability](#exceptions-retries-and-reliability)
|
@@ -319,6 +320,35 @@ GoodJob includes a Dashboard as a mountable `Rails::Engine`.
|
|
319
320
|
end
|
320
321
|
```
|
321
322
|
|
323
|
+
### ActiveJob Concurrency
|
324
|
+
|
325
|
+
GoodJob can extend ActiveJob to provide limits on concurrently running jobs, either at time of _enqueue_ or at _perform_.
|
326
|
+
|
327
|
+
**Note:** Limiting concurrency at _enqueue_ requires Rails 6.0+ because Rails 5.2 does not support `throw :abort` in ActiveJob callbacks.
|
328
|
+
|
329
|
+
```ruby
|
330
|
+
class MyJob < ApplicationJob
|
331
|
+
include GoodJob::ActiveJobExtensions::Concurrency
|
332
|
+
|
333
|
+
good_job_control_concurrency_with(
|
334
|
+
# Maximum number of jobs with the concurrency key to be concurrently enqueued
|
335
|
+
enqueue_limit: 2,
|
336
|
+
|
337
|
+
# Maximum number of jobs with the concurrency key to be concurrently performed
|
338
|
+
perform_limit: 1,
|
339
|
+
|
340
|
+
# A unique key to be globally locked against.
|
341
|
+
# Can be String or Lambda/Proc that is invoked in the context of the job.
|
342
|
+
# Note: Arguments passed to #perform_later must be accessed through `arguments` method.
|
343
|
+
key: -> { "Unique-#{arguments.first}" } # MyJob.perform_later("Alice") => "Unique-Alice"
|
344
|
+
)
|
345
|
+
|
346
|
+
def perform(first_name)
|
347
|
+
# do work
|
348
|
+
end
|
349
|
+
end
|
350
|
+
```
|
351
|
+
|
322
352
|
### Updating
|
323
353
|
|
324
354
|
GoodJob follows semantic versioning, though updates may be encouraged through deprecation warnings in minor versions.
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module GoodJob
|
2
|
+
module ActiveJobExtensions
|
3
|
+
module Concurrency
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
ConcurrencyExceededError = Class.new(StandardError)
|
7
|
+
|
8
|
+
included do
|
9
|
+
class_attribute :good_job_concurrency_config, instance_accessor: false, default: {}
|
10
|
+
|
11
|
+
before_enqueue do |job|
|
12
|
+
# Always allow jobs to be retried because the current job's execution will complete momentarily
|
13
|
+
next if CurrentExecution.active_job_id == job.job_id
|
14
|
+
|
15
|
+
limit = job.class.good_job_concurrency_config.fetch(:enqueue_limit, Float::INFINITY)
|
16
|
+
next if limit.blank? || (0...Float::INFINITY).exclude?(limit)
|
17
|
+
|
18
|
+
key = job.good_job_concurrency_key
|
19
|
+
next if key.blank?
|
20
|
+
|
21
|
+
GoodJob::Job.new.with_advisory_lock(key: key, function: "pg_advisory_lock") do
|
22
|
+
# TODO: Why is `unscoped` necessary? Nested scope is bleeding into subsequent query?
|
23
|
+
enqueue_concurrency = GoodJob::Job.unscoped.where(concurrency_key: key).unfinished.count
|
24
|
+
# The job has not yet been enqueued, so check if adding it will go over the limit
|
25
|
+
throw :abort if enqueue_concurrency + 1 > limit
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
retry_on(
|
30
|
+
GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError,
|
31
|
+
attempts: Float::INFINITY,
|
32
|
+
wait: :exponentially_longer
|
33
|
+
)
|
34
|
+
|
35
|
+
before_perform do |job|
|
36
|
+
limit = job.class.good_job_concurrency_config.fetch(:perform_limit, Float::INFINITY)
|
37
|
+
next if limit.blank? || (0...Float::INFINITY).exclude?(limit)
|
38
|
+
|
39
|
+
key = job.good_job_concurrency_key
|
40
|
+
next if key.blank?
|
41
|
+
|
42
|
+
GoodJob::Job.new.with_advisory_lock(key: key, function: "pg_advisory_lock") do
|
43
|
+
perform_concurrency = GoodJob::Job.unscoped.where(concurrency_key: key).advisory_locked.count
|
44
|
+
# The current job has already been locked and will appear in the previous query
|
45
|
+
raise GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError if perform_concurrency > limit
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class_methods do
|
51
|
+
def good_job_control_concurrency_with(config)
|
52
|
+
self.good_job_concurrency_config = config
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def good_job_concurrency_key
|
57
|
+
key = self.class.good_job_concurrency_config[:key]
|
58
|
+
return if key.blank?
|
59
|
+
|
60
|
+
if key.respond_to? :call
|
61
|
+
instance_exec(&key)
|
62
|
+
else
|
63
|
+
key
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -4,11 +4,11 @@ module GoodJob
|
|
4
4
|
# Thread-local attributes for passing values from Instrumentation.
|
5
5
|
# (Cannot use ActiveSupport::CurrentAttributes because ActiveJob resets it)
|
6
6
|
module CurrentExecution
|
7
|
-
# @!attribute [rw]
|
7
|
+
# @!attribute [rw] active_job_id
|
8
8
|
# @!scope class
|
9
|
-
#
|
10
|
-
# @return [
|
11
|
-
thread_mattr_accessor :
|
9
|
+
# ActiveJob ID
|
10
|
+
# @return [String, nil]
|
11
|
+
thread_mattr_accessor :active_job_id
|
12
12
|
|
13
13
|
# @!attribute [rw] error_on_discard
|
14
14
|
# @!scope class
|
@@ -16,11 +16,18 @@ module GoodJob
|
|
16
16
|
# @return [Exception, nil]
|
17
17
|
thread_mattr_accessor :error_on_discard
|
18
18
|
|
19
|
+
# @!attribute [rw] error_on_retry
|
20
|
+
# @!scope class
|
21
|
+
# Error captured by retry_on
|
22
|
+
# @return [Exception, nil]
|
23
|
+
thread_mattr_accessor :error_on_retry
|
24
|
+
|
19
25
|
# Resets attributes
|
20
26
|
# @return [void]
|
21
27
|
def self.reset
|
22
|
-
self.
|
28
|
+
self.active_job_id = nil
|
23
29
|
self.error_on_discard = nil
|
30
|
+
self.error_on_retry = nil
|
24
31
|
end
|
25
32
|
|
26
33
|
# @return [Integer] Current process ID
|
data/lib/good_job/job.rb
CHANGED
@@ -219,6 +219,21 @@ module GoodJob
|
|
219
219
|
DEPRECATION
|
220
220
|
end
|
221
221
|
|
222
|
+
if column_names.include?('concurrency_key')
|
223
|
+
good_job_args[:concurrency_key] = active_job.good_job_concurrency_key if active_job.respond_to?(:good_job_concurrency_key)
|
224
|
+
else
|
225
|
+
ActiveSupport::Deprecation.warn(<<~DEPRECATION)
|
226
|
+
GoodJob has pending database migrations. To create the migration files, run:
|
227
|
+
|
228
|
+
rails generate good_job:update
|
229
|
+
|
230
|
+
To apply the migration files, run:
|
231
|
+
|
232
|
+
rails db:migrate
|
233
|
+
|
234
|
+
DEPRECATION
|
235
|
+
end
|
236
|
+
|
222
237
|
good_job = GoodJob::Job.new(**good_job_args)
|
223
238
|
|
224
239
|
instrument_payload[:good_job] = good_job
|
@@ -264,6 +279,10 @@ module GoodJob
|
|
264
279
|
self.class.unscoped.unfinished.owns_advisory_locked.exists?(id: id)
|
265
280
|
end
|
266
281
|
|
282
|
+
def active_job_id
|
283
|
+
super || serialized_params['job_id']
|
284
|
+
end
|
285
|
+
|
267
286
|
private
|
268
287
|
|
269
288
|
# @return [ExecutionResult]
|
@@ -273,6 +292,7 @@ module GoodJob
|
|
273
292
|
)
|
274
293
|
|
275
294
|
GoodJob::CurrentExecution.reset
|
295
|
+
GoodJob::CurrentExecution.active_job_id = active_job_id
|
276
296
|
ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do
|
277
297
|
value = ActiveJob::Base.execute(params)
|
278
298
|
|
data/lib/good_job/lockable.rb
CHANGED
@@ -201,9 +201,16 @@ module GoodJob
|
|
201
201
|
# @param function [String, Symbol] Postgres Advisory Lock function name to use
|
202
202
|
# @return [Boolean] whether the lock was acquired.
|
203
203
|
def advisory_lock(key: lockable_key, function: advisory_lockable_function)
|
204
|
-
query =
|
205
|
-
|
206
|
-
|
204
|
+
query = if function.include? "_try_"
|
205
|
+
<<~SQL.squish
|
206
|
+
SELECT #{function}(('x'||substr(md5($1::text), 1, 16))::bit(64)::bigint) AS locked
|
207
|
+
SQL
|
208
|
+
else
|
209
|
+
<<~SQL.squish
|
210
|
+
SELECT #{function}(('x'||substr(md5($1::text), 1, 16))::bit(64)::bigint)::text AS locked
|
211
|
+
SQL
|
212
|
+
end
|
213
|
+
|
207
214
|
binds = [[nil, key]]
|
208
215
|
self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Lock', binds).first['locked']
|
209
216
|
end
|
data/lib/good_job/version.rb
CHANGED
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: 1.
|
4
|
+
version: 1.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Sheldon
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-07-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -362,6 +362,8 @@ files:
|
|
362
362
|
- lib/generators/good_job/templates/update/migrations/03_add_active_job_id_index_and_concurrency_key_index_to_good_jobs.rb
|
363
363
|
- lib/generators/good_job/update_generator.rb
|
364
364
|
- lib/good_job.rb
|
365
|
+
- lib/good_job/active_job_extensions.rb
|
366
|
+
- lib/good_job/active_job_extensions/concurrency.rb
|
365
367
|
- lib/good_job/adapter.rb
|
366
368
|
- lib/good_job/cli.rb
|
367
369
|
- lib/good_job/configuration.rb
|