good_job 3.8.0 → 3.9.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: d5d0c455a8d5d8f18fda38f1f9f126cc406250d188ee313d0352717c829cd126
4
- data.tar.gz: 208b61ec3b48a7973b160093100b216eafbb96eee604e5e7edc2e7fb67bedfba
3
+ metadata.gz: 69560b4af2d1cab2a3783fb80573794066c3704aacafa7bd315a9dc6e1b75441
4
+ data.tar.gz: 55dff98e3c8ebbcebd5f09664d02f4ddd23b298c0ee2bc7cb163e43f6963de96
5
5
  SHA512:
6
- metadata.gz: cac4f2c135e6c01cbdd6cd58c591d726ee8fbb66e623e3d1ed8bfb26e85e6b058e45bd60e3d2ba67b3133666df07d972db8eca75aa05df671b8284c01f45398f
7
- data.tar.gz: 189ba8af30dc9e5a84c064090d5ede8a8f1da4bb56d2249f6ac4130ce3105c9c498f957ad05cf054f2697f83885570013c3c41ed5d197354c992b08518110656
6
+ metadata.gz: dd4763e6d473a22ea7b0f6f7b501249c7e5e29d06c1fdd9b3d028c923ba407fb485eea748fd37857a5eb09f5b26659d67f4792f4a66848542bfc1f47f7384577
7
+ data.tar.gz: 89e165d003d156fa16450762b8a8a269172509557ab59be60e7d54794d079c9963f8c11f45047ecfdea8e0963d901c8d9e1c297743392a05817d21d417c78dab
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [v3.9.0](https://github.com/bensheldon/good_job/tree/v3.9.0) (2023-01-31)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.8.0...v3.9.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Abort enqueue when the concurrency limit is reached [\#820](https://github.com/bensheldon/good_job/pull/820) ([TAGraves](https://github.com/TAGraves))
10
+ - Add bulk enqueue functionality [\#790](https://github.com/bensheldon/good_job/pull/790) ([julik](https://github.com/julik))
11
+
12
+ **Merged pull requests:**
13
+
14
+ - Bump alex-page/github-project-automation-plus from 0.8.2 to 0.8.3 [\#819](https://github.com/bensheldon/good_job/pull/819) ([dependabot[bot]](https://github.com/apps/dependabot))
15
+ - Bump concurrent-ruby from 1.1.10 to 1.2.0 [\#818](https://github.com/bensheldon/good_job/pull/818) ([dependabot[bot]](https://github.com/apps/dependabot))
16
+ - Bump rails from 6.1.7 to 6.1.7.2 [\#817](https://github.com/bensheldon/good_job/pull/817) ([dependabot[bot]](https://github.com/apps/dependabot))
17
+ - Bump selenium-webdriver from 4.7.1 to 4.8.0 [\#816](https://github.com/bensheldon/good_job/pull/816) ([dependabot[bot]](https://github.com/apps/dependabot))
18
+ - Bump rubocop from 1.43.0 to 1.44.1 [\#815](https://github.com/bensheldon/good_job/pull/815) ([dependabot[bot]](https://github.com/apps/dependabot))
19
+ - Ensure that anytime the Notifier uses autoloaded constants \(ActiveRecord\), they are wrapped with a Rails Executor [\#797](https://github.com/bensheldon/good_job/pull/797) ([bensheldon](https://github.com/bensheldon))
20
+ - Remove support for Ruby 2.5 and JRuby 9.2; reactivate appraisal tests for Rails HEAD [\#756](https://github.com/bensheldon/good_job/pull/756) ([bensheldon](https://github.com/bensheldon))
21
+
3
22
  ## [v3.8.0](https://github.com/bensheldon/good_job/tree/v3.8.0) (2023-01-27)
4
23
 
5
24
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.7.4...v3.8.0)
@@ -68,13 +87,16 @@
68
87
 
69
88
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.7.1...v3.7.2)
70
89
 
90
+ **Fixed bugs:**
91
+
92
+ - Ignore ActiveJob::DeserializationError when discarding jobs [\#771](https://github.com/bensheldon/good_job/pull/771) ([nickcampbell18](https://github.com/nickcampbell18))
93
+
71
94
  **Closed issues:**
72
95
 
73
96
  - Unable to discard failed jobs which crashed with `ActiveJob::DeserializationError` [\#770](https://github.com/bensheldon/good_job/issues/770)
74
97
 
75
98
  **Merged pull requests:**
76
99
 
77
- - Ignore ActiveJob::DeserializationError when discarding jobs [\#771](https://github.com/bensheldon/good_job/pull/771) ([nickcampbell18](https://github.com/nickcampbell18))
78
100
  - Bump rubocop from 1.39.0 to 1.40.0 [\#769](https://github.com/bensheldon/good_job/pull/769) ([dependabot[bot]](https://github.com/apps/dependabot))
79
101
 
80
102
  ## [v3.7.1](https://github.com/bensheldon/good_job/tree/v3.7.1) (2022-12-12)
data/README.md CHANGED
@@ -58,6 +58,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
58
58
  - [Database connections](#database-connections)
59
59
  - [Production setup](#production-setup)
60
60
  - [Queue performance with Queue Select Limit](#queue-performance-with-queue-select-limit)
61
+ - [Bulk enqueue](#bulk-enqueue)
61
62
  - [Execute jobs async / in-process](#execute-jobs-async--in-process)
62
63
  - [Migrate to GoodJob from a different ActiveJob backend](#migrate-to-goodjob-from-a-different-activejob-backend)
63
64
  - [Monitor and preserve worked jobs](#monitor-and-preserve-worked-jobs)
@@ -146,7 +147,7 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
146
147
  ## Compatibility
147
148
 
148
149
  - **Ruby on Rails:** 6.0+
149
- - **Ruby:** Ruby 2.5+. JRuby 9.2.13+
150
+ - **Ruby:** Ruby 2.6+. JRuby 9.3+
150
151
  - **Postgres:** 10.0+
151
152
 
152
153
  ## Configuration
@@ -793,6 +794,26 @@ To explain where this value is used, here is the pseudo-query that GoodJob uses
793
794
  )
794
795
  ```
795
796
 
797
+ ### Bulk enqueue
798
+
799
+ GoodJob's Bulk-enqueue functionality can buffer and enqueue multiple jobs at once, using a single INSERT statement. This can more performant when enqueuing a large number of jobs.
800
+
801
+ ```ruby
802
+ # Capture jobs using `.perform_later`:
803
+ active_jobs = GoodJob::Bulk.enqueue do
804
+ MyJob.perform_later
805
+ AnotherJob.perform_later
806
+ # If an exception is raised within this block, no jobs will be inserted.
807
+ end
808
+
809
+ # All ActiveJob instances are returned from GoodJob::Bulk.enqueue.
810
+ # Jobs that have been successfully enqueued have a `provider_job_id` set.
811
+ active_jobs.all?(&:provider_job_id)
812
+
813
+ # Bulk enqueue ActiveJob instances directly without using `.perform_later`:
814
+ GoodJob::Bulk.enqueue(MyJob.new, AnotherJob.new)
815
+ ```
816
+
796
817
  ### Execute jobs async / in-process
797
818
 
798
819
  GoodJob can execute jobs "async" in the same process as the web server (e.g. `bin/rails s`). GoodJob's async execution mode offers benefits of economy by not requiring a separate job worker process, but with the tradeoff of increased complexity. Async mode can be configured in two ways:
@@ -43,10 +43,10 @@ module GoodJob
43
43
  case string.first
44
44
  when '-'
45
45
  exclude_queues = true
46
- string = string[1..-1]
46
+ string = string[1..]
47
47
  when '+'
48
48
  ordered_queues = true
49
- string = string[1..-1]
49
+ string = string[1..]
50
50
  end
51
51
 
52
52
  queues = string.split(',').map(&:strip)
@@ -197,6 +197,28 @@ module GoodJob
197
197
  end
198
198
  end)
199
199
 
200
+ # Construct a GoodJob::Execution from an ActiveJob instance.
201
+ def self.build_for_enqueue(active_job, overrides = {})
202
+ execution_args = {
203
+ active_job_id: active_job.job_id,
204
+ queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
205
+ priority: active_job.priority || DEFAULT_PRIORITY,
206
+ serialized_params: active_job.serialize,
207
+ scheduled_at: active_job.scheduled_at,
208
+ }
209
+
210
+ execution_args[:concurrency_key] = active_job.good_job_concurrency_key if active_job.respond_to?(:good_job_concurrency_key)
211
+
212
+ if CurrentThread.cron_key
213
+ execution_args[:cron_key] = CurrentThread.cron_key
214
+ execution_args[:cron_at] = CurrentThread.cron_at
215
+ elsif CurrentThread.active_job_id && CurrentThread.active_job_id == active_job.job_id
216
+ execution_args[:cron_key] = CurrentThread.execution.cron_key
217
+ end
218
+
219
+ new(**execution_args.merge(overrides))
220
+ end
221
+
200
222
  # Finds the next eligible Execution, acquire an advisory lock related to it, and
201
223
  # executes the job.
202
224
  # @return [ExecutionResult, nil]
@@ -244,33 +266,19 @@ module GoodJob
244
266
  # @param active_job [ActiveJob::Base]
245
267
  # The job to enqueue.
246
268
  # @param scheduled_at [Float]
247
- # Epoch timestamp when the job should be executed.
269
+ # Epoch timestamp when the job should be executed, if blank will delegate to the ActiveJob instance
248
270
  # @param create_with_advisory_lock [Boolean]
249
271
  # Whether to establish a lock on the {Execution} record after it is created.
272
+ # @param persist_immediately [Boolean]
273
+ # Whether to save the record immediately or just initialize it with values. When bulk-inserting
274
+ # jobs the caller takes care of the persistence and sets this parameter to `false`
250
275
  # @return [Execution]
251
276
  # The new {Execution} instance representing the queued ActiveJob job.
252
277
  def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false)
253
278
  ActiveSupport::Notifications.instrument("enqueue_job.good_job", { active_job: active_job, scheduled_at: scheduled_at, create_with_advisory_lock: create_with_advisory_lock }) do |instrument_payload|
254
- execution_args = {
255
- active_job_id: active_job.job_id,
256
- queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME,
257
- priority: active_job.priority || DEFAULT_PRIORITY,
258
- serialized_params: active_job.serialize,
259
- scheduled_at: scheduled_at,
260
- create_with_advisory_lock: create_with_advisory_lock,
261
- }
262
-
263
- execution_args[:concurrency_key] = active_job.good_job_concurrency_key if active_job.respond_to?(:good_job_concurrency_key)
264
-
265
- if CurrentThread.cron_key
266
- execution_args[:cron_key] = CurrentThread.cron_key
267
- execution_args[:cron_at] = CurrentThread.cron_at
268
- elsif CurrentThread.active_job_id && CurrentThread.active_job_id == active_job.job_id
269
- execution_args[:cron_key] = CurrentThread.execution.cron_key
270
- end
271
-
272
- execution = GoodJob::Execution.new(**execution_args)
279
+ execution = build_for_enqueue(active_job, { scheduled_at: scheduled_at })
273
280
 
281
+ execution.create_with_advisory_lock = create_with_advisory_lock
274
282
  instrument_payload[:execution] = execution
275
283
 
276
284
  execution.save!
@@ -25,40 +25,13 @@ module GoodJob
25
25
  class_attribute :good_job_concurrency_config, instance_accessor: false, default: {}
26
26
  attr_writer :good_job_concurrency_key
27
27
 
28
- around_enqueue do |job, block|
29
- # Don't attempt to enforce concurrency limits with other queue adapters.
30
- next(block.call) unless job.class.queue_adapter.is_a?(GoodJob::Adapter)
31
-
32
- # Always allow jobs to be retried because the current job's execution will complete momentarily
33
- next(block.call) if CurrentThread.active_job_id == job.job_id
34
-
35
- # Only generate the concurrency key on the initial enqueue in case it is dynamic
36
- job.good_job_concurrency_key ||= job._good_job_concurrency_key
37
- key = job.good_job_concurrency_key
38
- next(block.call) if key.blank?
39
-
40
- enqueue_limit = job.class.good_job_concurrency_config[:enqueue_limit]
41
- enqueue_limit = instance_exec(&enqueue_limit) if enqueue_limit.respond_to?(:call)
42
- enqueue_limit = nil unless enqueue_limit.present? && (0...Float::INFINITY).cover?(enqueue_limit)
43
-
44
- unless enqueue_limit
45
- total_limit = job.class.good_job_concurrency_config[:total_limit]
46
- total_limit = instance_exec(&total_limit) if total_limit.respond_to?(:call)
47
- total_limit = nil unless total_limit.present? && (0...Float::INFINITY).cover?(total_limit)
28
+ if ActiveJob.gem_version >= Gem::Version.new("6.1.0")
29
+ before_enqueue do |job|
30
+ good_job_enqueue_concurrency_check(job, on_abort: -> { throw(:abort) }, on_enqueue: nil)
48
31
  end
49
-
50
- limit = enqueue_limit || total_limit
51
- next(block.call) unless limit
52
-
53
- GoodJob::Execution.advisory_lock_key(key, function: "pg_advisory_lock") do
54
- enqueue_concurrency = if enqueue_limit
55
- GoodJob::Execution.where(concurrency_key: key).unfinished.advisory_unlocked.count
56
- else
57
- GoodJob::Execution.where(concurrency_key: key).unfinished.count
58
- end
59
-
60
- # The job has not yet been enqueued, so check if adding it will go over the limit
61
- block.call unless (enqueue_concurrency + 1) > limit
32
+ else
33
+ around_enqueue do |job, block|
34
+ good_job_enqueue_concurrency_check(job, on_abort: nil, on_enqueue: block)
62
35
  end
63
36
  end
64
37
 
@@ -113,6 +86,50 @@ module GoodJob
113
86
  @good_job_concurrency_key || _good_job_concurrency_key
114
87
  end
115
88
 
89
+ private
90
+
91
+ def good_job_enqueue_concurrency_check(job, on_abort:, on_enqueue:)
92
+ # Don't attempt to enforce concurrency limits with other queue adapters.
93
+ return on_enqueue&.call unless job.class.queue_adapter.is_a?(GoodJob::Adapter)
94
+
95
+ # Always allow jobs to be retried because the current job's execution will complete momentarily
96
+ return on_enqueue&.call if CurrentThread.active_job_id == job.job_id
97
+
98
+ # Only generate the concurrency key on the initial enqueue in case it is dynamic
99
+ job.good_job_concurrency_key ||= job._good_job_concurrency_key
100
+ key = job.good_job_concurrency_key
101
+ return on_enqueue&.call if key.blank?
102
+
103
+ enqueue_limit = job.class.good_job_concurrency_config[:enqueue_limit]
104
+ enqueue_limit = instance_exec(&enqueue_limit) if enqueue_limit.respond_to?(:call)
105
+ enqueue_limit = nil unless enqueue_limit.present? && (0...Float::INFINITY).cover?(enqueue_limit)
106
+
107
+ unless enqueue_limit
108
+ total_limit = job.class.good_job_concurrency_config[:total_limit]
109
+ total_limit = instance_exec(&total_limit) if total_limit.respond_to?(:call)
110
+ total_limit = nil unless total_limit.present? && (0...Float::INFINITY).cover?(total_limit)
111
+ end
112
+
113
+ limit = enqueue_limit || total_limit
114
+ return on_enqueue&.call unless limit
115
+
116
+ GoodJob::Execution.advisory_lock_key(key, function: "pg_advisory_lock") do
117
+ enqueue_concurrency = if enqueue_limit
118
+ GoodJob::Execution.where(concurrency_key: key).unfinished.advisory_unlocked.count
119
+ else
120
+ GoodJob::Execution.where(concurrency_key: key).unfinished.count
121
+ end
122
+
123
+ # The job has not yet been enqueued, so check if adding it will go over the limit
124
+ if (enqueue_concurrency + 1) > limit
125
+ logger.info "Aborted enqueue of #{job.class.name} (Job ID: #{job.job_id}) because the concurrency key '#{key}' has reached its limit of #{limit} #{'job'.pluralize(limit)}"
126
+ on_abort&.call
127
+ else
128
+ on_enqueue&.call
129
+ end
130
+ end
131
+ end
132
+
116
133
  # Generates the concurrency key from the configuration
117
134
  # @return [Object] concurrency key
118
135
  def _good_job_concurrency_key
@@ -40,6 +40,70 @@ module GoodJob
40
40
  enqueue_at(active_job, nil)
41
41
  end
42
42
 
43
+ # Enqueues multiple ActiveJob instances at once
44
+ # @param active_jobs [Array<ActiveJob::Base>] jobs to be enqueued
45
+ # @return [Integer] number of jobs that were successfully enqueued
46
+ def enqueue_all(active_jobs)
47
+ active_jobs = Array(active_jobs)
48
+ return 0 if active_jobs.empty?
49
+
50
+ current_time = Time.current
51
+ executions = active_jobs.map do |active_job|
52
+ GoodJob::Execution.build_for_enqueue(active_job, {
53
+ id: SecureRandom.uuid,
54
+ created_at: current_time,
55
+ updated_at: current_time,
56
+ })
57
+ end
58
+
59
+ inline_executions = []
60
+ GoodJob::Execution.transaction(requires_new: true, joinable: false) do
61
+ results = GoodJob::Execution.insert_all(executions.map(&:attributes), returning: %w[id active_job_id]) # rubocop:disable Rails/SkipsModelValidations
62
+
63
+ job_id_to_provider_job_id = results.each_with_object({}) { |result, hash| hash[result['active_job_id']] = result['id'] }
64
+ active_jobs.each do |active_job|
65
+ active_job.provider_job_id = job_id_to_provider_job_id[active_job.job_id]
66
+ end
67
+ executions.each do |execution|
68
+ execution.instance_variable_set(:@new_record, false) if job_id_to_provider_job_id[execution.active_job_id]
69
+ end
70
+ executions = executions.select(&:persisted?) # prune unpersisted executions
71
+
72
+ if execute_inline?
73
+ inline_executions = executions.select { |execution| (execution.scheduled_at.nil? || execution.scheduled_at <= Time.current) }
74
+ inline_executions.each(&:advisory_lock!)
75
+ end
76
+ end
77
+
78
+ begin
79
+ until inline_executions.empty?
80
+ begin
81
+ inline_execution = inline_executions.shift
82
+ inline_result = inline_execution.perform
83
+ ensure
84
+ inline_execution.advisory_unlock
85
+ inline_execution.run_callbacks(:perform_unlocked)
86
+ end
87
+ raise inline_result.unhandled_error if inline_result.unhandled_error
88
+ end
89
+ ensure
90
+ inline_executions.each(&:advisory_unlock)
91
+ end
92
+
93
+ executions.reject(&:finished_at).group_by(&:queue_name).each do |queue_name, executions_by_queue|
94
+ executions_by_queue.group_by(&:scheduled_at).each do |scheduled_at, executions_by_queue_and_scheduled_at|
95
+ # TODO: have Adapter#create_thread handle state[:count] values
96
+ state = { queue_name: queue_name, count: executions_by_queue_and_scheduled_at.size }
97
+ state[:scheduled_at] = scheduled_at if scheduled_at
98
+
99
+ executed_locally = execute_async? && @scheduler&.create_thread(state)
100
+ Notifier.notify(state) unless executed_locally
101
+ end
102
+ end
103
+
104
+ active_jobs.count(&:provider_job_id)
105
+ end
106
+
43
107
  # Enqueues an ActiveJob job to be run at a specific time.
44
108
  # For use by Rails; you should generally not call this directly.
45
109
  # @param active_job [ActiveJob::Base] the job to be enqueued from +#perform_later+
@@ -47,8 +111,12 @@ module GoodJob
47
111
  # @return [GoodJob::Execution]
48
112
  def enqueue_at(active_job, timestamp)
49
113
  scheduled_at = timestamp ? Time.zone.at(timestamp) : nil
50
- will_execute_inline = execute_inline? && (scheduled_at.nil? || scheduled_at <= Time.current)
51
114
 
115
+ # If there is a currently open Bulk in the current thread, direct the
116
+ # job there to be enqueued using enqueue_all
117
+ return if GoodJob::Bulk.capture(active_job, queue_adapter: self)
118
+
119
+ will_execute_inline = execute_inline? && (scheduled_at.nil? || scheduled_at <= Time.current)
52
120
  execution = GoodJob::Execution.enqueue(
53
121
  active_job,
54
122
  scheduled_at: scheduled_at,
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/core_ext/module/attribute_accessors_per_thread'
3
+
4
+ module GoodJob
5
+ module Bulk
6
+ Error = Class.new(StandardError)
7
+
8
+ # @!attribute [rw] current_buffer
9
+ # @!scope class
10
+ # Current buffer of jobs to be enqueued.
11
+ # @return [GoodJob::Bulk::Buffer, nil]
12
+ thread_mattr_accessor :current_buffer
13
+
14
+ # Capture jobs to a buffer. Pass either a block, or specific Active Jobs to be buffered.
15
+ # @param active_jobs [Array<ActiveJob::Base>] Active Jobs to be buffered.
16
+ # @param queue_adapter Override the jobs implict queue adapter with an explicit one.
17
+ # @return [nil, Array<ActiveJob::Base>] The ActiveJob instances that have been buffered; nil if no active buffer
18
+ def self.capture(active_jobs = nil, queue_adapter: nil)
19
+ raise(ArgumentError, "Use either the block form or the argument form, not both") if block_given? && active_jobs
20
+
21
+ if block_given?
22
+ begin
23
+ original_buffer = current_buffer
24
+ self.current_buffer = Buffer.new
25
+ yield
26
+ current_buffer.active_jobs
27
+ ensure
28
+ self.current_buffer = original_buffer
29
+ end
30
+ else
31
+ current_buffer&.add(active_jobs, queue_adapter: queue_adapter)
32
+ end
33
+ end
34
+
35
+ # Capture jobs to a buffer and enqueue them all at once; or enqueue the current buffer.
36
+ # @param active_jobs [Array<ActiveJob::Base>] Active Jobs to be enqueued.
37
+ # @return [Array<ActiveJob::Base>] The ActiveJob instances that have been captured; check provider_job_id to confirm enqueued.
38
+ def self.enqueue(active_jobs = nil)
39
+ raise(ArgumentError, "Use either the block form or the argument form, not both") if block_given? && active_jobs
40
+
41
+ if block_given?
42
+ capture do
43
+ yield
44
+ current_buffer&.enqueue
45
+ end
46
+ elsif active_jobs.present?
47
+ buffer = Buffer.new
48
+ buffer.add(active_jobs)
49
+ buffer.enqueue
50
+ buffer.active_jobs
51
+ elsif current_buffer.present?
52
+ current_buffer.enqueue
53
+ current_buffer.active_jobs
54
+ end
55
+ end
56
+
57
+ # Temporarily unset the current buffer; used to enqueue buffered jobs.
58
+ # @return [void]
59
+ def self.unbuffer
60
+ original_buffer = current_buffer
61
+ self.current_buffer = nil
62
+ yield
63
+ ensure
64
+ self.current_buffer = original_buffer
65
+ end
66
+
67
+ class Buffer
68
+ def initialize
69
+ @values = []
70
+ end
71
+
72
+ def add(active_jobs, queue_adapter: nil)
73
+ new_pairs = Array(active_jobs).map do |active_job|
74
+ adapter = queue_adapter || active_job.class.queue_adapter
75
+ raise Error, "Jobs must have a Queue Adapter" unless adapter
76
+
77
+ [active_job, adapter]
78
+ end
79
+ @values.append(*new_pairs)
80
+
81
+ true
82
+ end
83
+
84
+ def enqueue
85
+ Bulk.unbuffer do
86
+ active_jobs_by_queue_adapter.each do |adapter, jobs|
87
+ jobs = jobs.reject(&:provider_job_id) # Do not re-enqueue already enqueued jobs
88
+
89
+ if adapter.respond_to?(:enqueue_all)
90
+ unbulkable_jobs, bulkable_jobs = jobs.partition do |job|
91
+ job.respond_to?(:good_job_concurrency_key) && job.good_job_concurrency_key &&
92
+ (job.class.good_job_concurrency_config[:enqueue_limit] || job.class.good_job_concurrency_config[:total_limit])
93
+ end
94
+ adapter.enqueue_all(bulkable_jobs) if bulkable_jobs.any?
95
+ else
96
+ unbulkable_jobs = jobs
97
+ end
98
+
99
+ unbulkable_jobs.each do |job|
100
+ job.enqueue
101
+ rescue GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError
102
+ # ignore
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def active_jobs_by_queue_adapter
109
+ @values.each_with_object({}) do |(job, adapter), memo|
110
+ memo[adapter] ||= []
111
+ memo[adapter] << job
112
+ end
113
+ end
114
+
115
+ def active_jobs
116
+ @values.map(&:first)
117
+ end
118
+ end
119
+ end
120
+ end
@@ -168,35 +168,37 @@ module GoodJob # :nodoc:
168
168
  future = Concurrent::ScheduledTask.new(delay, args: [@recipients, executor, @listening], executor: @executor) do |thr_recipients, thr_executor, thr_listening|
169
169
  with_connection do
170
170
  begin
171
- run_callbacks :listen do
172
- ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
173
- connection.execute("LISTEN #{CHANNEL}")
171
+ Rails.application.executor.wrap do
172
+ run_callbacks :listen do
173
+ ActiveSupport::Notifications.instrument("notifier_listen.good_job") do
174
+ connection.execute("LISTEN #{CHANNEL}")
175
+ end
176
+ thr_listening.make_true
174
177
  end
175
- thr_listening.make_true
176
178
  end
177
179
 
178
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
179
- while thr_executor.running?
180
- wait_for_notify do |channel, payload|
181
- next unless channel == CHANNEL
182
-
183
- ActiveSupport::Notifications.instrument("notifier_notified.good_job", { payload: payload })
184
- parsed_payload = JSON.parse(payload, symbolize_names: true)
185
- thr_recipients.each do |recipient|
186
- target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
187
- target.send(method_name, parsed_payload)
188
- end
189
- end
180
+ while thr_executor.running?
181
+ wait_for_notify do |channel, payload|
182
+ next unless channel == CHANNEL
190
183
 
191
- reset_connection_errors
184
+ ActiveSupport::Notifications.instrument("notifier_notified.good_job", { payload: payload })
185
+ parsed_payload = JSON.parse(payload, symbolize_names: true)
186
+ thr_recipients.each do |recipient|
187
+ target, method_name = recipient.is_a?(Array) ? recipient : [recipient, :call]
188
+ target.send(method_name, parsed_payload)
189
+ end
192
190
  end
191
+
192
+ reset_connection_errors
193
193
  end
194
194
  end
195
195
  ensure
196
- run_callbacks :unlisten do
197
- thr_listening.make_false
198
- ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
199
- connection.execute("UNLISTEN *")
196
+ Rails.application.executor.wrap do
197
+ run_callbacks :unlisten do
198
+ thr_listening.make_false
199
+ ActiveSupport::Notifications.instrument("notifier_unlisten.good_job") do
200
+ connection.execute("UNLISTEN *")
201
+ end
200
202
  end
201
203
  end
202
204
  end
@@ -207,8 +209,10 @@ module GoodJob # :nodoc:
207
209
  end
208
210
 
209
211
  def with_connection
210
- self.connection = Execution.connection_pool.checkout.tap do |conn|
211
- Execution.connection_pool.remove(conn)
212
+ Rails.application.executor.wrap do
213
+ self.connection = Execution.connection_pool.checkout.tap do |conn|
214
+ Execution.connection_pool.remove(conn)
215
+ end
212
216
  end
213
217
  connection.execute("SET application_name = #{connection.quote(self.class.name)}")
214
218
 
@@ -155,6 +155,19 @@ module GoodJob # :nodoc:
155
155
  if state
156
156
  return false unless performer.next?(state)
157
157
 
158
+ if state[:count]
159
+ # When given state for multiple jobs, try to create a thread for each one.
160
+ # Return true if a thread can be created for all of them, nil if partial or none.
161
+
162
+ state_without_count = state.without(:count)
163
+ result = state[:count].times do
164
+ value = create_thread(state_without_count)
165
+ break(value) unless value
166
+ end
167
+
168
+ return result.nil? ? nil : true
169
+ end
170
+
158
171
  if state[:scheduled_at]
159
172
  scheduled_at = if state[:scheduled_at].is_a? String
160
173
  Time.zone.parse state[:scheduled_at]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
  module GoodJob
3
3
  # GoodJob gem version.
4
- VERSION = '3.8.0'
4
+ VERSION = '3.9.0'
5
5
  end
data/lib/good_job.rb CHANGED
@@ -10,6 +10,7 @@ require "active_job/queue_adapters/good_job_adapter"
10
10
  require "good_job/active_job_extensions/concurrency"
11
11
 
12
12
  require "good_job/assignable_connection"
13
+ require "good_job/bulk"
13
14
  require "good_job/cleanup_tracker"
14
15
  require "good_job/cli"
15
16
  require "good_job/configuration"
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.8.0
4
+ version: 3.9.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-01-27 00:00:00.000000000 Z
11
+ date: 2023-01-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -136,20 +136,6 @@ dependencies:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
- - !ruby/object:Gem::Dependency
140
- name: database_cleaner
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - ">="
151
- - !ruby/object:Gem::Version
152
- version: '0'
153
139
  - !ruby/object:Gem::Dependency
154
140
  name: dotenv
155
141
  requirement: !ruby/object:Gem::Requirement
@@ -409,6 +395,7 @@ files:
409
395
  - lib/good_job/active_job_extensions/concurrency.rb
410
396
  - lib/good_job/adapter.rb
411
397
  - lib/good_job/assignable_connection.rb
398
+ - lib/good_job/bulk.rb
412
399
  - lib/good_job/cleanup_tracker.rb
413
400
  - lib/good_job/cli.rb
414
401
  - lib/good_job/configuration.rb
@@ -451,14 +438,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
451
438
  requirements:
452
439
  - - ">="
453
440
  - !ruby/object:Gem::Version
454
- version: 2.5.0
441
+ version: 2.6.0
455
442
  required_rubygems_version: !ruby/object:Gem::Requirement
456
443
  requirements:
457
444
  - - ">="
458
445
  - !ruby/object:Gem::Version
459
446
  version: '0'
460
447
  requirements: []
461
- rubygems_version: 3.1.6
448
+ rubygems_version: 3.4.5
462
449
  signing_key:
463
450
  specification_version: 4
464
451
  summary: A multithreaded, Postgres-based ActiveJob backend for Ruby on Rails