good_job 3.19.3 → 3.20.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: cb4912917e34844ff32647321159e6c45ea03d962d94b10ee28186cceab9371b
4
- data.tar.gz: 1fb418aa9060e1e5781e044fb33ea27be557e1cc171cb82fe78fc1cb345c3e48
3
+ metadata.gz: 62d33742278a734dd514fc3147abd1393ee62c3fd644c4c065b10c4fc6a24755
4
+ data.tar.gz: 83d56214ec454cbc3d910d4cbefb0413877ca3b18577168c0d9b9b456385a7a3
5
5
  SHA512:
6
- metadata.gz: ad52e1fe639545fcf8d3ef4d3a3e70727493edf397127bda1a68b10535511033620cb7dcc3945c352a1203e765882ac741bf4c4333d9a976f2e9863dc4ce1c11
7
- data.tar.gz: 6a6851b391e7651f404ddf42c067d5e2d74380775514799ac159c93f1d63748dc4025fb89e89c1af6f5db8cd14acf3f071dc6354bd3956c8ba7abe0eb29a2674
6
+ metadata.gz: 90a3730d4989f26837b0ac9cd8f15ea3029f0c1c474159a4798a58d695153cb2067c336d382f971ba42a6faf371f677f7b295741c8ab1557add7c6b403a1fa94
7
+ data.tar.gz: 684e5d8473b4f460707394fd670067b0aaf7a2749ed525be2893967c0953ed1c5afe4f35e8acfce4acaa7f7fa39ae00d16394b38d186a11f7eb8812839d65ff3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [v3.20.0](https://github.com/bensheldon/good_job/tree/v3.20.0) (2023-10-23)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.19.4...v3.20.0)
6
+
7
+ **Closed issues:**
8
+
9
+ - Dashboard blocked in iframe tag [\#1111](https://github.com/bensheldon/good_job/issues/1111)
10
+ - PG::ConnectionBad: PQsocket\(\) can't get socket descriptor [\#1100](https://github.com/bensheldon/good_job/issues/1100)
11
+
12
+ **Merged pull requests:**
13
+
14
+ - Wrap Adapter enqueue methods and Batch callbacks with Rails Reloader; verify in tests that no Advisory locks remain at database connection check-in [\#1124](https://github.com/bensheldon/good_job/pull/1124) ([bensheldon](https://github.com/bensheldon))
15
+ - Run all RSpec examples within a Rails Executor [\#1122](https://github.com/bensheldon/good_job/pull/1122) ([bensheldon](https://github.com/bensheldon))
16
+ - Print better debugging for retained advisory locks in test [\#1121](https://github.com/bensheldon/good_job/pull/1121) ([bensheldon](https://github.com/bensheldon))
17
+ - Replace Heroku-specific Rake tasks with `db:prepare` now that Demo is upgraded to Rails 7.1 [\#1120](https://github.com/bensheldon/good_job/pull/1120) ([bensheldon](https://github.com/bensheldon))
18
+ - Do not error debug logs if `pg_stat_activity` join is empty [\#1119](https://github.com/bensheldon/good_job/pull/1119) ([bensheldon](https://github.com/bensheldon))
19
+ - Remove pinned psych version [\#1114](https://github.com/bensheldon/good_job/pull/1114) ([bensheldon](https://github.com/bensheldon))
20
+ - Remove `pg_advisory_unlock_all()` after job is run; only verify blank `finished_at` \(and not lock presence\) before performing job [\#1113](https://github.com/bensheldon/good_job/pull/1113) ([bensheldon](https://github.com/bensheldon))
21
+ - Update docs url [\#1112](https://github.com/bensheldon/good_job/pull/1112) ([ur5us](https://github.com/ur5us))
22
+ - Pin psych gem 5.1.0 [\#1108](https://github.com/bensheldon/good_job/pull/1108) ([bensheldon](https://github.com/bensheldon))
23
+ - Add sampling for Skylight traces on Demo [\#1107](https://github.com/bensheldon/good_job/pull/1107) ([bensheldon](https://github.com/bensheldon))
24
+ - Add Rails 7.1 to test matrix [\#1105](https://github.com/bensheldon/good_job/pull/1105) ([bensheldon](https://github.com/bensheldon))
25
+ - Add spec to verify unhandled thread errors are reported [\#1104](https://github.com/bensheldon/good_job/pull/1104) ([bensheldon](https://github.com/bensheldon))
26
+ - Update Codespace configuration [\#1101](https://github.com/bensheldon/good_job/pull/1101) ([bensheldon](https://github.com/bensheldon))
27
+
28
+ ## [v3.19.4](https://github.com/bensheldon/good_job/tree/v3.19.4) (2023-10-04)
29
+
30
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.19.3...v3.19.4)
31
+
32
+ **Closed issues:**
33
+
34
+ - Including GoodJob::ActiveJobExtensions::Concurrency triggers rails deprecation notice [\#1096](https://github.com/bensheldon/good_job/issues/1096)
35
+ - Add retries exhaused callback [\#1080](https://github.com/bensheldon/good_job/issues/1080)
36
+
37
+ **Merged pull requests:**
38
+
39
+ - Address rails 7.1 deprecation when using `ActiveJobExtensions::Concurrency` [\#1097](https://github.com/bensheldon/good_job/pull/1097) ([Earlopain](https://github.com/Earlopain))
40
+
3
41
  ## [v3.19.3](https://github.com/bensheldon/good_job/tree/v3.19.3) (2023-09-28)
4
42
 
5
43
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.19.2...v3.19.3)
data/README.md CHANGED
@@ -816,7 +816,7 @@ GoodJob.on_thread_error = -> (exception) { Rails.error.report(exception) }
816
816
 
817
817
  By default, GoodJob relies on ActiveJob's retry functionality.
818
818
 
819
- ActiveJob can be configured to retry an infinite number of times, with an exponential backoff. Using ActiveJob's `retry_on` prevents exceptions from reaching GoodJob:
819
+ ActiveJob can be configured to retry an infinite number of times, with a polynomial backoff. Using ActiveJob's `retry_on` prevents exceptions from reaching GoodJob:
820
820
 
821
821
  ```ruby
822
822
  class ApplicationJob < ActiveJob::Base
@@ -53,7 +53,7 @@ module GoodJob
53
53
  composed_cte = Arel::Nodes::As.new(cte_table, Arel::Nodes::SqlLiteral.new([cte_type, "(", cte_query.to_sql, ")"].join(' ')))
54
54
  query = cte_table.project(cte_table[:id])
55
55
  .with(composed_cte)
56
- .where(Arel.sql(sanitize_sql_for_conditions(["#{function}(('x' || substr(md5(:table_name || '-' || #{connection.quote_table_name(cte_table.name)}.#{connection.quote_column_name(column)}::text), 1, 16))::bit(64)::bigint)", { table_name: table_name }])))
56
+ .where(Arel.sql("#{function}(('x' || substr(md5(#{connection.quote(table_name)} || '-' || #{connection.quote_table_name(cte_table.name)}.#{connection.quote_column_name(column)}::text), 1, 16))::bit(64)::bigint)"))
57
57
 
58
58
  limit = original_query.arel.ast.limit
59
59
  query.limit = limit.value if limit.present?
@@ -74,14 +74,12 @@ module GoodJob
74
74
  # @example Get the records that have a session awaiting a lock:
75
75
  # MyLockableRecord.joins_advisory_locks.where("pg_locks.granted = ?", false)
76
76
  scope :joins_advisory_locks, (lambda do |column: _advisory_lockable_column|
77
- join_sql = <<~SQL.squish
77
+ joins(<<~SQL.squish)
78
78
  LEFT JOIN pg_locks ON pg_locks.locktype = 'advisory'
79
79
  AND pg_locks.objsubid = 1
80
- AND pg_locks.classid = ('x' || substr(md5(:table_name || '-' || #{quoted_table_name}.#{connection.quote_column_name(column)}::text), 1, 16))::bit(32)::int
81
- AND pg_locks.objid = (('x' || substr(md5(:table_name || '-' || #{quoted_table_name}.#{connection.quote_column_name(column)}::text), 1, 16))::bit(64) << 32)::bit(32)::int
80
+ AND pg_locks.classid = ('x' || substr(md5(#{connection.quote(table_name)} || '-' || #{quoted_table_name}.#{connection.quote_column_name(column)}::text), 1, 16))::bit(32)::int
81
+ AND pg_locks.objid = (('x' || substr(md5(#{connection.quote(table_name)} || '-' || #{quoted_table_name}.#{connection.quote_column_name(column)}::text), 1, 16))::bit(64) << 32)::bit(32)::int
82
82
  SQL
83
-
84
- joins(sanitize_sql_for_conditions([join_sql, { table_name: table_name }]))
85
83
  end)
86
84
 
87
85
  # Joins the current query with Postgres's +pg_locks+ table AND SELECTs the resulting columns
@@ -151,6 +149,10 @@ module GoodJob
151
149
  # can (as in {Lockable.advisory_lock}) and only pass those that could be
152
150
  # locked to the block.
153
151
  #
152
+ # If the Active Record Relation has WHERE conditions that have the potential
153
+ # to be updated/changed elsewhere, be sure to verify the conditions are still
154
+ # satisfied, or check the lock status, as an unlocked and out-of-date record could be returned.
155
+ #
154
156
  # @param column [String, Symbol] name of advisory lock or unlock function
155
157
  # @param function [String, Symbol] Postgres Advisory Lock function name to use
156
158
  # @param unlock_session [Boolean] Whether to unlock all advisory locks in the session afterwards
@@ -101,9 +101,13 @@ module GoodJob
101
101
 
102
102
  active_jobs = add(active_jobs, &block)
103
103
 
104
- record.with_advisory_lock(function: "pg_advisory_lock") do
105
- record.update!(enqueued_at: Time.current)
106
- record._continue_discard_or_finish(lock: false)
104
+ Rails.application.reloader.wrap do
105
+ record.with_advisory_lock(function: "pg_advisory_lock") do
106
+ record.update!(enqueued_at: Time.current)
107
+
108
+ # During inline execution, this could enqueue and execute further jobs
109
+ record._continue_discard_or_finish(lock: false)
110
+ end
107
111
  end
108
112
 
109
113
  active_jobs
@@ -256,12 +256,13 @@ module GoodJob
256
256
  def self.perform_with_advisory_lock(parsed_queues: nil, queue_select_limit: nil)
257
257
  execution = nil
258
258
  result = nil
259
- unfinished.dequeueing_ordered(parsed_queues).only_scheduled.limit(1).with_advisory_lock(unlock_session: true, select_limit: queue_select_limit) do |executions|
259
+ unfinished.dequeueing_ordered(parsed_queues).only_scheduled.limit(1).with_advisory_lock(select_limit: queue_select_limit) do |executions|
260
260
  execution = executions.first
261
261
  break if execution.blank?
262
262
 
263
263
  unless execution.executable?
264
264
  result = ExecutionResult.new(value: nil, unexecutable: true)
265
+ execution = nil
265
266
  break
266
267
  end
267
268
 
@@ -491,7 +492,9 @@ module GoodJob
491
492
  # Tests whether this job is safe to be executed by this thread.
492
493
  # @return [Boolean]
493
494
  def executable?
494
- self.class.unscoped.unfinished.owns_advisory_locked.exists?(id: id)
495
+ reload.finished_at.blank?
496
+ rescue ActiveRecord::RecordNotFound
497
+ false
495
498
  end
496
499
 
497
500
  def make_discrete
@@ -36,10 +36,16 @@ module GoodJob
36
36
  end
37
37
  end
38
38
 
39
+ wait_key = if ActiveJob.gem_version >= Gem::Version.new("7.1.0.a")
40
+ :polynomially_longer
41
+ else
42
+ :exponentially_longer
43
+ end
44
+
39
45
  retry_on(
40
46
  GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError,
41
47
  attempts: Float::INFINITY,
42
- wait: :exponentially_longer
48
+ wait: wait_key
43
49
  )
44
50
 
45
51
  before_perform do |job|
@@ -49,74 +49,76 @@ module GoodJob
49
49
  active_jobs = Array(active_jobs)
50
50
  return 0 if active_jobs.empty?
51
51
 
52
- current_time = Time.current
53
- executions = active_jobs.map do |active_job|
54
- GoodJob::Execution.build_for_enqueue(active_job).tap do |execution|
55
- if GoodJob::Execution.discrete_support?
56
- execution.make_discrete
57
- execution.scheduled_at = current_time if execution.scheduled_at == execution.created_at
58
- end
52
+ Rails.application.reloader.wrap do
53
+ current_time = Time.current
54
+ executions = active_jobs.map do |active_job|
55
+ GoodJob::Execution.build_for_enqueue(active_job).tap do |execution|
56
+ if GoodJob::Execution.discrete_support?
57
+ execution.make_discrete
58
+ execution.scheduled_at = current_time if execution.scheduled_at == execution.created_at
59
+ end
59
60
 
60
- execution.created_at = current_time
61
- execution.updated_at = current_time
61
+ execution.created_at = current_time
62
+ execution.updated_at = current_time
63
+ end
62
64
  end
63
- end
64
65
 
65
- inline_executions = []
66
- GoodJob::Execution.transaction(requires_new: true, joinable: false) do
67
- execution_attributes = executions.map do |execution|
68
- if GoodJob::Execution.error_event_migrated?
69
- execution.attributes
70
- else
71
- execution.attributes.except('error_event')
66
+ inline_executions = []
67
+ GoodJob::Execution.transaction(requires_new: true, joinable: false) do
68
+ execution_attributes = executions.map do |execution|
69
+ if GoodJob::Execution.error_event_migrated?
70
+ execution.attributes
71
+ else
72
+ execution.attributes.except('error_event')
73
+ end
72
74
  end
73
- end
74
75
 
75
- results = GoodJob::Execution.insert_all(execution_attributes, returning: %w[id active_job_id]) # rubocop:disable Rails/SkipsModelValidations
76
+ results = GoodJob::Execution.insert_all(execution_attributes, returning: %w[id active_job_id]) # rubocop:disable Rails/SkipsModelValidations
76
77
 
77
- job_id_to_provider_job_id = results.each_with_object({}) { |result, hash| hash[result['active_job_id']] = result['id'] }
78
- active_jobs.each do |active_job|
79
- active_job.provider_job_id = job_id_to_provider_job_id[active_job.job_id]
80
- active_job.successfully_enqueued = active_job.provider_job_id.present? if active_job.respond_to?(:successfully_enqueued=)
81
- end
82
- executions.each do |execution|
83
- execution.instance_variable_set(:@new_record, false) if job_id_to_provider_job_id[execution.active_job_id]
84
- end
85
- executions = executions.select(&:persisted?) # prune unpersisted executions
78
+ job_id_to_provider_job_id = results.each_with_object({}) { |result, hash| hash[result['active_job_id']] = result['id'] }
79
+ active_jobs.each do |active_job|
80
+ active_job.provider_job_id = job_id_to_provider_job_id[active_job.job_id]
81
+ active_job.successfully_enqueued = active_job.provider_job_id.present? if active_job.respond_to?(:successfully_enqueued=)
82
+ end
83
+ executions.each do |execution|
84
+ execution.instance_variable_set(:@new_record, false) if job_id_to_provider_job_id[execution.active_job_id]
85
+ end
86
+ executions = executions.select(&:persisted?) # prune unpersisted executions
86
87
 
87
- if execute_inline?
88
- inline_executions = executions.select { |execution| (execution.scheduled_at.nil? || execution.scheduled_at <= Time.current) }
89
- inline_executions.each(&:advisory_lock!)
88
+ if execute_inline?
89
+ inline_executions = executions.select { |execution| (execution.scheduled_at.nil? || execution.scheduled_at <= Time.current) }
90
+ inline_executions.each(&:advisory_lock!)
91
+ end
90
92
  end
91
- end
92
93
 
93
- begin
94
- until inline_executions.empty?
95
- begin
96
- inline_execution = inline_executions.shift
97
- inline_result = inline_execution.perform
98
- ensure
99
- inline_execution.advisory_unlock
100
- inline_execution.run_callbacks(:perform_unlocked)
94
+ begin
95
+ until inline_executions.empty?
96
+ begin
97
+ inline_execution = inline_executions.shift
98
+ inline_result = inline_execution.perform
99
+ ensure
100
+ inline_execution.advisory_unlock
101
+ inline_execution.run_callbacks(:perform_unlocked)
102
+ end
103
+ raise inline_result.unhandled_error if inline_result.unhandled_error
101
104
  end
102
- raise inline_result.unhandled_error if inline_result.unhandled_error
105
+ ensure
106
+ inline_executions.each(&:advisory_unlock)
103
107
  end
104
- ensure
105
- inline_executions.each(&:advisory_unlock)
106
- end
107
108
 
108
- non_inline_executions = executions.reject(&:finished_at)
109
- if non_inline_executions.any?
110
- job_id_to_active_jobs = active_jobs.index_by(&:job_id)
111
- non_inline_executions.group_by(&:queue_name).each do |queue_name, executions_by_queue|
112
- executions_by_queue.group_by(&:scheduled_at).each do |scheduled_at, executions_by_queue_and_scheduled_at|
113
- state = { queue_name: queue_name, count: executions_by_queue_and_scheduled_at.size }
114
- state[:scheduled_at] = scheduled_at if scheduled_at
115
-
116
- executed_locally = execute_async? && @capsule&.create_thread(state)
117
- unless executed_locally
118
- state[:count] = job_id_to_active_jobs.values_at(*executions_by_queue_and_scheduled_at.map(&:active_job_id)).count { |active_job| send_notify?(active_job) }
119
- Notifier.notify(state) unless state[:count].zero?
109
+ non_inline_executions = executions.reject(&:finished_at)
110
+ if non_inline_executions.any?
111
+ job_id_to_active_jobs = active_jobs.index_by(&:job_id)
112
+ non_inline_executions.group_by(&:queue_name).each do |queue_name, executions_by_queue|
113
+ executions_by_queue.group_by(&:scheduled_at).each do |scheduled_at, executions_by_queue_and_scheduled_at|
114
+ state = { queue_name: queue_name, count: executions_by_queue_and_scheduled_at.size }
115
+ state[:scheduled_at] = scheduled_at if scheduled_at
116
+
117
+ executed_locally = execute_async? && @capsule&.create_thread(state)
118
+ unless executed_locally
119
+ state[:count] = job_id_to_active_jobs.values_at(*executions_by_queue_and_scheduled_at.map(&:active_job_id)).count { |active_job| send_notify?(active_job) }
120
+ Notifier.notify(state) unless state[:count].zero?
121
+ end
120
122
  end
121
123
  end
122
124
  end
@@ -137,30 +139,32 @@ module GoodJob
137
139
  # job there to be enqueued using enqueue_all
138
140
  return if GoodJob::Bulk.capture(active_job, queue_adapter: self)
139
141
 
140
- will_execute_inline = execute_inline? && (scheduled_at.nil? || scheduled_at <= Time.current)
141
- execution = GoodJob::Execution.enqueue(
142
- active_job,
143
- scheduled_at: scheduled_at,
144
- create_with_advisory_lock: will_execute_inline
145
- )
142
+ Rails.application.reloader.wrap do
143
+ will_execute_inline = execute_inline? && (scheduled_at.nil? || scheduled_at <= Time.current)
144
+ execution = GoodJob::Execution.enqueue(
145
+ active_job,
146
+ scheduled_at: scheduled_at,
147
+ create_with_advisory_lock: will_execute_inline
148
+ )
146
149
 
147
- if will_execute_inline
148
- begin
149
- result = execution.perform
150
- ensure
151
- execution.advisory_unlock
152
- execution.run_callbacks(:perform_unlocked)
150
+ if will_execute_inline
151
+ begin
152
+ result = execution.perform
153
+ ensure
154
+ execution.advisory_unlock
155
+ execution.run_callbacks(:perform_unlocked)
156
+ end
157
+ raise result.unhandled_error if result.unhandled_error
158
+ else
159
+ job_state = { queue_name: execution.queue_name }
160
+ job_state[:scheduled_at] = execution.scheduled_at if execution.scheduled_at
161
+
162
+ executed_locally = execute_async? && @capsule&.create_thread(job_state)
163
+ Notifier.notify(job_state) if !executed_locally && send_notify?(active_job)
153
164
  end
154
- raise result.unhandled_error if result.unhandled_error
155
- else
156
- job_state = { queue_name: execution.queue_name }
157
- job_state[:scheduled_at] = execution.scheduled_at if execution.scheduled_at
158
165
 
159
- executed_locally = execute_async? && @capsule&.create_thread(job_state)
160
- Notifier.notify(job_state) if !executed_locally && send_notify?(active_job)
166
+ execution
161
167
  end
162
-
163
- execution
164
168
  end
165
169
 
166
170
  # Shut down the thread pool executors.
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GoodJob
4
4
  # GoodJob gem version.
5
- VERSION = '3.19.3'
5
+ VERSION = '3.20.0'
6
6
 
7
7
  # GoodJob version as Gem::Version object
8
8
  GEM_VERSION = Gem::Version.new(VERSION)
data/lib/good_job.rb CHANGED
@@ -237,7 +237,7 @@ module GoodJob
237
237
  def self.perform_inline(queue_string = "*")
238
238
  job_performer = JobPerformer.new(queue_string)
239
239
  loop do
240
- result = job_performer.next
240
+ result = Rails.application.reloader.wrap { job_performer.next }
241
241
  break unless result
242
242
  raise result.unhandled_error if result.unhandled_error
243
243
  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: 3.19.3
4
+ version: 3.20.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: 2023-09-28 00:00:00.000000000 Z
11
+ date: 2023-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -384,7 +384,7 @@ licenses:
384
384
  metadata:
385
385
  bug_tracker_uri: https://github.com/bensheldon/good_job/issues
386
386
  changelog_uri: https://github.com/bensheldon/good_job/blob/master/CHANGELOG.md
387
- documentation_uri: https://rdoc.info/github/bensheldon/good_job
387
+ documentation_uri: https://rubydoc.info/gems/good_job
388
388
  homepage_uri: https://github.com/bensheldon/good_job
389
389
  source_code_uri: https://github.com/bensheldon/good_job
390
390
  rubygems_mfa_required: 'true'