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 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