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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2621d47f3d0de0bb0411e1b6984e5e684ec2b54a218a29e3c01219995c46f53
4
- data.tar.gz: 7772377aeb51daed1530d7f01fcfe92d06ba7ca992ff010846e595450a8bd6b2
3
+ metadata.gz: 6c0b45fcd80fbd536af0f6fed44ef53f446ee6574f7e90d41a38585ec8b3ba63
4
+ data.tar.gz: 8b45f77d94bd809b25fb725f57622f1966f188157f235e03c56442817e31afd4
5
5
  SHA512:
6
- metadata.gz: 1f6640c27c6f3d7e2b2c20952b655e70d1079fa59cf5b130c403cf613b200902c8e5b57d36647747165cec2ec944e183f0720974c8a73bfaab3c64968003ce91
7
- data.tar.gz: b514789fd4df9589f012f2c2156bfaef0fd05d2106a10345e23105e6260a160816d7a8b4ba5d97eb9095ba917ecaf3d0630e9c121cb0a23a0563dbc641cd8f45
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,4 @@
1
+ module GoodJob
2
+ module ActiveJobExtensions
3
+ end
4
+ end
@@ -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] error_on_retry
7
+ # @!attribute [rw] active_job_id
8
8
  # @!scope class
9
- # Error captured by retry_on
10
- # @return [Exception, nil]
11
- thread_mattr_accessor :error_on_retry
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.error_on_retry = nil
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
 
@@ -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 = <<~SQL.squish
205
- SELECT #{function}(('x'||substr(md5($1::text), 1, 16))::bit(64)::bigint) AS locked
206
- SQL
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
@@ -1,4 +1,4 @@
1
1
  module GoodJob
2
2
  # GoodJob gem version.
3
- VERSION = '1.10.1'.freeze
3
+ VERSION = '1.11.0'.freeze
4
4
  end
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.10.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-06-30 00:00:00.000000000 Z
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