good_job 1.10.1 → 1.11.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|