good_job 2.0.5 → 2.1.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: fe9ce3416f1a816d3aa317139da71333f36c69dec121371c163a4965d61a2a6a
4
- data.tar.gz: 1d3cd7fd9bb43ccc0b0f94bbdf99cf2cd3865f4316ea8178aa67e4c3dfb9a97b
3
+ metadata.gz: a15eb306ad205685ef586d40c1f54cea489e900143e7080fa3da72b3272846a6
4
+ data.tar.gz: 4739ce28860fda800a785d32802a94083f7012496f465360240e895b15533879
5
5
  SHA512:
6
- metadata.gz: 6aa38c7714d0e5e959d0dfcd48fc9cfcbbae475d40cdd9d9f3aa452178b470de500cc1c0513c00a80e4d8b2e762c73b2a55b1233a638d6acb7c3320ecdb16975
7
- data.tar.gz: 96ec7cc7fbdb4cdc232a1ddfd046df5aa7976336e639d77fe3ff8dfdc9d2814cebdf88d8450aab2afa7f80a4b911282b96632fe04602c6dba5548a4435fce41f
6
+ metadata.gz: 4f02dbda1d52e6340b9cb93b3dd5389271f1356844391c4c14ffcc651b25edd7cc1d6bcdfe96f164e7ee8faec47b00daca5e2cd0d11ec2a616a15450801425ce
7
+ data.tar.gz: 335871d05b792214b2a168726c154c7267d5c6081b448cd1623462d11013d45bbb0b49adc5aea488de6e13f74f556ec8b1134684b1e5505f334324dfeeb948e8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [v2.1.0](https://github.com/bensheldon/good_job/tree/v2.1.0) (2021-09-09)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.0.5...v2.1.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Add `total_limit:` option to GoodJob::Concurrency to be inclusive of counting both enqueued and performing jobs [\#369](https://github.com/bensheldon/good_job/pull/369) ([bensheldon](https://github.com/bensheldon))
10
+ - Add button to toggle all job params in Dashboard [\#365](https://github.com/bensheldon/good_job/pull/365) ([bensheldon](https://github.com/bensheldon))
11
+
12
+ **Closed issues:**
13
+
14
+ - Cron-like jobs not always executed, possible reasons? [\#359](https://github.com/bensheldon/good_job/issues/359)
15
+
16
+ **Merged pull requests:**
17
+
18
+ - When shelling out in tests, send SIGKILL if process does not exit [\#371](https://github.com/bensheldon/good_job/pull/371) ([bensheldon](https://github.com/bensheldon))
19
+ - Fix unlock key for Lockable\#with\_advisory\_lock [\#368](https://github.com/bensheldon/good_job/pull/368) ([bensheldon](https://github.com/bensheldon))
20
+ - Have all tests use stubbed TestJob [\#364](https://github.com/bensheldon/good_job/pull/364) ([bensheldon](https://github.com/bensheldon))
21
+
3
22
  ## [v2.0.5](https://github.com/bensheldon/good_job/tree/v2.0.5) (2021-09-06)
4
23
 
5
24
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v2.0.4...v2.0.5)
data/README.md CHANGED
@@ -348,17 +348,20 @@ GoodJob includes a Dashboard as a mountable `Rails::Engine`.
348
348
 
349
349
  GoodJob can extend ActiveJob to provide limits on concurrently running jobs, either at time of _enqueue_ or at _perform_. Limiting concurrency can help prevent duplicate, double or unecessary jobs from being enqueued, or race conditions when performing, for example when interacting with 3rd-party APIs.
350
350
 
351
- **Note:** Limiting concurrency at _enqueue_ requires Rails 6.0+ because Rails 5.2 does not support `throw :abort` in ActiveJob callbacks.
351
+ **Note:** Limiting concurrency at _enqueue_ requires Rails 6.0+ because Rails 5.2 cannot halt ActiveJob callbacks.
352
352
 
353
353
  ```ruby
354
354
  class MyJob < ApplicationJob
355
355
  include GoodJob::ActiveJobExtensions::Concurrency
356
356
 
357
357
  good_job_control_concurrency_with(
358
- # Maximum number of jobs with the concurrency key to be concurrently enqueued
359
- enqueue_limit: 2,
358
+ # Maximum number of unfinished jobs to allow with the concurrency key
359
+ total_limit: 1,
360
360
 
361
- # Maximum number of jobs with the concurrency key to be concurrently performed
361
+ # Or, if more control is needed:
362
+ # Maximum number of jobs with the concurrency key to be concurrently enqueued (excludes performing jobs)
363
+ enqueue_limit: 2,
364
+ # Maximum number of jobs with the concurrency key to be concurrently performed (excludes enqueued jobs)
362
365
  perform_limit: 1,
363
366
 
364
367
  # A unique key to be globally locked against.
@@ -2,14 +2,22 @@
2
2
  <div class="table-responsive">
3
3
  <table class="table card-table table-bordered table-hover table-sm mb-0">
4
4
  <thead>
5
- <th>GoodJob ID</th>
6
- <th>ActiveJob ID</th>
7
- <th>Job Class</th>
8
- <th>Queue</th>
9
- <th>Scheduled At</th>
10
- <th>Error</th>
11
- <th>ActiveJob Params</th>
12
- <th>Actions</th>
5
+ <tr>
6
+ <th>GoodJob ID</th>
7
+ <th>ActiveJob ID</th>
8
+ <th>Job Class</th>
9
+ <th>Queue</th>
10
+ <th>Scheduled At</th>
11
+ <th>Error</th>
12
+ <th>
13
+ ActiveJob Params&nbsp;
14
+ <%= tag.button "Toggle", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
15
+ data: { bs_toggle: "collapse", bs_target: ".job-params" },
16
+ aria: { expanded: false, controls: jobs.map { |job| "##{dom_id(job, "params")}" }.join(" ") }
17
+ %>
18
+ </th>
19
+ <th>Actions</th>
20
+ </tr>
13
21
  </thead>
14
22
  <tbody>
15
23
  <% jobs.each do |job| %>
@@ -22,10 +30,10 @@
22
30
  <td class="text-break"><%= truncate(job.error, length: 1_000) %></td>
23
31
  <td>
24
32
  <%= tag.button "Preview", type: "button", class: "btn btn-sm btn-outline-primary", role: "button",
25
- data: {bs_toggle: "collapse", bs_target: "##{dom_id(job, 'params')}"},
26
- aria: {expanded: false, controls: dom_id(job, "params")}
33
+ data: { bs_toggle: "collapse", bs_target: "##{dom_id(job, 'params')}" },
34
+ aria: { expanded: false, controls: dom_id(job, "params") }
27
35
  %>
28
- <%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse" %>
36
+ <%= tag.pre JSON.pretty_generate(job.serialized_params), id: dom_id(job, "params"), class: "collapse job-params" %>
29
37
  </td>
30
38
  <td>
31
39
  <%= button_to job_path(job.id), method: :delete, class: "btn btn-sm btn-outline-danger", title: "Delete job" do %>
@@ -20,17 +20,26 @@ module GoodJob
20
20
  # Always allow jobs to be retried because the current job's execution will complete momentarily
21
21
  next(block.call) if CurrentExecution.active_job_id == job.job_id
22
22
 
23
- limit = job.class.good_job_concurrency_config.fetch(:enqueue_limit, Float::INFINITY)
24
- next(block.call) if limit.blank? || (0...Float::INFINITY).exclude?(limit)
23
+ enqueue_limit = job.class.good_job_concurrency_config[:enqueue_limit]
24
+ total_limit = job.class.good_job_concurrency_config[:total_limit]
25
+
26
+ has_limit = (enqueue_limit.present? && (0...Float::INFINITY).cover?(enqueue_limit)) ||
27
+ (total_limit.present? && (0...Float::INFINITY).cover?(total_limit))
28
+ next(block.call) unless has_limit
25
29
 
26
30
  key = job.good_job_concurrency_key
27
31
  next(block.call) if key.blank?
28
32
 
29
33
  GoodJob::Job.new.with_advisory_lock(key: key, function: "pg_advisory_lock") do
30
- # TODO: Why is `unscoped` necessary? Nested scope is bleeding into subsequent query?
31
- enqueue_concurrency = GoodJob::Job.unscoped.where(concurrency_key: key).unfinished.advisory_unlocked.count
34
+ enqueue_concurrency = if enqueue_limit
35
+ # TODO: Why is `unscoped` necessary? Nested scope is bleeding into subsequent query?
36
+ GoodJob::Job.unscoped.where(concurrency_key: key).unfinished.advisory_unlocked.count
37
+ else
38
+ GoodJob::Job.unscoped.where(concurrency_key: key).unfinished.count
39
+ end
40
+
32
41
  # The job has not yet been enqueued, so check if adding it will go over the limit
33
- block.call unless enqueue_concurrency + 1 > limit
42
+ block.call unless enqueue_concurrency + 1 > (enqueue_limit || total_limit)
34
43
  end
35
44
  end
36
45
 
@@ -44,14 +53,17 @@ module GoodJob
44
53
  # Don't attempt to enforce concurrency limits with other queue adapters.
45
54
  next unless job.class.queue_adapter.is_a?(GoodJob::Adapter)
46
55
 
47
- limit = job.class.good_job_concurrency_config.fetch(:perform_limit, Float::INFINITY)
48
- next if limit.blank? || (0...Float::INFINITY).exclude?(limit)
56
+ perform_limit = job.class.good_job_concurrency_config[:perform_limit] ||
57
+ job.class.good_job_concurrency_config[:total_limit]
58
+
59
+ has_limit = perform_limit.present? && (0...Float::INFINITY).cover?(perform_limit)
60
+ next unless has_limit
49
61
 
50
62
  key = job.good_job_concurrency_key
51
63
  next if key.blank?
52
64
 
53
65
  GoodJob::Job.new.with_advisory_lock(key: key, function: "pg_advisory_lock") do
54
- allowed_active_job_ids = GoodJob::Job.unscoped.where(concurrency_key: key).advisory_locked.order(Arel.sql("COALESCE(performed_at, scheduled_at, created_at) ASC")).limit(limit).pluck(:active_job_id)
66
+ allowed_active_job_ids = GoodJob::Job.unscoped.where(concurrency_key: key).advisory_locked.order(Arel.sql("COALESCE(performed_at, scheduled_at, created_at) ASC")).limit(perform_limit).pluck(:active_job_id)
55
67
  # The current job has already been locked and will appear in the previous query
56
68
  raise GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError unless allowed_active_job_ids.include? job.job_id
57
69
  end
@@ -155,8 +155,7 @@ module GoodJob
155
155
  advisory_unlock_session
156
156
  else
157
157
  records.each do |record|
158
- key = [table_name, record[_advisory_lockable_column]].join
159
- record.advisory_unlock(key: key, function: advisory_unlockable_function(function))
158
+ record.advisory_unlock(key: record.lockable_column_key(column: column), function: advisory_unlockable_function(function))
160
159
  end
161
160
  end
162
161
  end
@@ -286,6 +285,13 @@ module GoodJob
286
285
  self.class.connection.exec_query(pg_or_jdbc_query(query), 'GoodJob::Lockable Advisory Locked?', binds).any?
287
286
  end
288
287
 
288
+ # Tests whether this record does not have an advisory lock on it.
289
+ # @param key [String, Symbol] Key to test lock against
290
+ # @return [Boolean]
291
+ def advisory_unlocked?(key: lockable_key)
292
+ !advisory_locked?(key: key)
293
+ end
294
+
289
295
  # Tests whether this record is locked by the current database session.
290
296
  # @param key [String, Symbol] Key to test lock against
291
297
  # @return [Boolean]
@@ -315,7 +321,13 @@ module GoodJob
315
321
  # Default Advisory Lock key
316
322
  # @return [String]
317
323
  def lockable_key
318
- "#{self.class.table_name}-#{self[self.class._advisory_lockable_column]}"
324
+ lockable_column_key
325
+ end
326
+
327
+ # Default Advisory Lock key for column-based locking
328
+ # @return [String]
329
+ def lockable_column_key(column: self.class._advisory_lockable_column)
330
+ "#{self.class.table_name}-#{self[column]}"
319
331
  end
320
332
 
321
333
  delegate :pg_or_jdbc_query, to: :class
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '2.0.5'
4
+ VERSION = '2.1.0'
5
5
  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: 2.0.5
4
+ version: 2.1.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-09-06 00:00:00.000000000 Z
11
+ date: 2021-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob