good_job 3.19.4 → 3.21.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: 7b2fd15bbee1a5d3cbe17a0154fa15fdd960c9b5fc39a55b60831792095538e0
4
- data.tar.gz: 831a78bc9a7e569fb72601f817bb4397b702ad89dc4f1e5776b3c8fc28ab042e
3
+ metadata.gz: f1e8a8a3ec28224d48f23c9c2795cd15f9eb515ba9059ab2bea4316a03b42aa9
4
+ data.tar.gz: e469a0f1be765789433ef9d0096c6c1c3fe3e87c90a184c11996a204a36dabd1
5
5
  SHA512:
6
- metadata.gz: c598627e0a4999e7faef09edb7c286344019a4c20fb7cf952bf0026a2247aa1f4aef4f9e9530e8cc105ec502cbf258f41f58ad5a1ec62ce7e2453f8dd4f0f92c
7
- data.tar.gz: 40dfbac0623a460c22f55889cd80bccc77a7391c3c05b42692267424c136af3224d20d042735906dca506540a699a5f7a592d8eda16ff79255089fc15b390829
6
+ metadata.gz: 6546caba43d15714d87c2b6f3a072b493804685411f7fdd3c73b8aaa0aacffb2f77ae8fcbb8f3eb0d9aeed9fbfab2c4175b49adebd3d5356a8994138b92e3852
7
+ data.tar.gz: 30f85524258b9f1b2b774601ef0fd23965ed21eac87b0a3c0491a94c0fe85a3c70efb57b9979d11047d8ea5176cf0008317c860c27411db86ecdf8ef2fd0a69c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # Changelog
2
2
 
3
+ ## [v3.21.0](https://github.com/bensheldon/good_job/tree/v3.21.0) (2023-11-06)
4
+
5
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.20.0...v3.21.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Add "cron enabled" column to processes index page [\#1127](https://github.com/bensheldon/good_job/pull/1127) ([bforma](https://github.com/bforma))
10
+ - Add `limit:` kwarg to `GoodJob.perform_inline` [\#1126](https://github.com/bensheldon/good_job/pull/1126) ([bensheldon](https://github.com/bensheldon))
11
+
12
+ **Closed issues:**
13
+
14
+ - Cron scheduler and multiple processes [\#1128](https://github.com/bensheldon/good_job/issues/1128)
15
+ - `GoodJob.on_thread_error` not called in tests [\#1102](https://github.com/bensheldon/good_job/issues/1102)
16
+
17
+ **Merged pull requests:**
18
+
19
+ - Use a Concurrent::Event for CLI signal-trapping loop [\#1141](https://github.com/bensheldon/good_job/pull/1141) ([bensheldon](https://github.com/bensheldon))
20
+ - Update README's optimize queue explanation [\#1138](https://github.com/bensheldon/good_job/pull/1138) ([maestromac](https://github.com/maestromac))
21
+ - Update development dependencies and light Rubocop'ing [\#1136](https://github.com/bensheldon/good_job/pull/1136) ([bensheldon](https://github.com/bensheldon))
22
+ - Move the Rails app harness from `spec/test_app` to `demo` [\#1135](https://github.com/bensheldon/good_job/pull/1135) ([bensheldon](https://github.com/bensheldon))
23
+ - In test, shutdown schedulers/capsules before doing assertions because of race conditions; store CI logs for Dev Env tests [\#1129](https://github.com/bensheldon/good_job/pull/1129) ([bensheldon](https://github.com/bensheldon))
24
+ - Use a constant to represent `None` for default/blank memoizable values [\#1125](https://github.com/bensheldon/good_job/pull/1125) ([bensheldon](https://github.com/bensheldon))
25
+
26
+ ## [v3.20.0](https://github.com/bensheldon/good_job/tree/v3.20.0) (2023-10-23)
27
+
28
+ [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.19.4...v3.20.0)
29
+
30
+ **Closed issues:**
31
+
32
+ - Dashboard blocked in iframe tag [\#1111](https://github.com/bensheldon/good_job/issues/1111)
33
+ - PG::ConnectionBad: PQsocket\(\) can't get socket descriptor [\#1100](https://github.com/bensheldon/good_job/issues/1100)
34
+
35
+ **Merged pull requests:**
36
+
37
+ - 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))
38
+ - Run all RSpec examples within a Rails Executor [\#1122](https://github.com/bensheldon/good_job/pull/1122) ([bensheldon](https://github.com/bensheldon))
39
+ - Print better debugging for retained advisory locks in test [\#1121](https://github.com/bensheldon/good_job/pull/1121) ([bensheldon](https://github.com/bensheldon))
40
+ - 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))
41
+ - 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))
42
+ - Remove pinned psych version [\#1114](https://github.com/bensheldon/good_job/pull/1114) ([bensheldon](https://github.com/bensheldon))
43
+ - 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))
44
+ - Update docs url [\#1112](https://github.com/bensheldon/good_job/pull/1112) ([ur5us](https://github.com/ur5us))
45
+ - Pin psych gem 5.1.0 [\#1108](https://github.com/bensheldon/good_job/pull/1108) ([bensheldon](https://github.com/bensheldon))
46
+ - Add sampling for Skylight traces on Demo [\#1107](https://github.com/bensheldon/good_job/pull/1107) ([bensheldon](https://github.com/bensheldon))
47
+ - Add Rails 7.1 to test matrix [\#1105](https://github.com/bensheldon/good_job/pull/1105) ([bensheldon](https://github.com/bensheldon))
48
+ - Add spec to verify unhandled thread errors are reported [\#1104](https://github.com/bensheldon/good_job/pull/1104) ([bensheldon](https://github.com/bensheldon))
49
+ - Update Codespace configuration [\#1101](https://github.com/bensheldon/good_job/pull/1101) ([bensheldon](https://github.com/bensheldon))
50
+
3
51
  ## [v3.19.4](https://github.com/bensheldon/good_job/tree/v3.19.4) (2023-10-04)
4
52
 
5
53
  [Full Changelog](https://github.com/bensheldon/good_job/compare/v3.19.3...v3.19.4)
data/README.md CHANGED
@@ -939,7 +939,7 @@ By default, GoodJob creates a single thread execution pool that will execute job
939
939
 
940
940
  - `transactional_messages:2`: execute jobs enqueued on `transactional_messages`, with up to 2 threads.
941
941
  - `batch_processing:1` execute jobs enqueued on `batch_processing`, with a single thread.
942
- - `-transactional_messages,batch_processing`: execute jobs enqueued on _any_ queue _excluding_ `transactional_messages` or `batch_processing`, with up to 2 threads.
942
+ - `-transactional_messages,batch_processing:2`: execute jobs enqueued on _any_ queue _excluding_ `transactional_messages` or `batch_processing`, with up to 2 threads.
943
943
  - `*`: execute jobs on any queue, with up to 5 threads (as configured by `--max-threads=5`).
944
944
 
945
945
  When a pool is performing jobs from multiple queues, jobs will be performed from specified queues, ordered by priority and creation time. To perform jobs from queues in the queues' given order, use the `+` modifier. In this example, jobs in `batch_processing` will be performed only when there are no jobs in `transactional_messages`:
@@ -1370,7 +1370,7 @@ bin/setup
1370
1370
 
1371
1371
  #### Rails development harness
1372
1372
 
1373
- A Rails application exists within `spec/test_app` that is used for development, test, and GoodJob Demo environments.
1373
+ A Rails application exists within `demo` that is used for development, test, and GoodJob Demo environments.
1374
1374
 
1375
1375
  ```bash
1376
1376
  # Run a local development webserver
@@ -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
@@ -8,6 +8,7 @@
8
8
  <div class="row small text-muted text-uppercase align-items-center">
9
9
  <div class="col"><%= t ".process" %></div>
10
10
  <div class="col"><%= t ".schedulers" %></div>
11
+ <div class="col-2 d-flex gap-2"><%= t ".cron_enabled" %></div>
11
12
  <div class="col-2 d-flex gap-2">
12
13
  <span><%= t ".started" %></span>
13
14
  </div>
@@ -44,6 +45,7 @@
44
45
  <pre class="mb-0"><%= scheduler.is_a?(Hash) ? scheduler['name'] : scheduler %></pre>
45
46
  <% end %>
46
47
  </div>
48
+ <div class="col-2 small"><%= t(ActiveModel::Type::Boolean.new.cast(process.state["cron_enabled"]), scope: "good_job.shared.boolean") %></div>
47
49
  <div class="col-2 small"><%= relative_time(process.created_at) %></div>
48
50
  <div class="col-2 small"><%= relative_time(process.updated_at) %></div>
49
51
  <div class="col-auto">
@@ -198,6 +198,7 @@ de:
198
198
  unit: ''
199
199
  processes:
200
200
  index:
201
+ cron_enabled: Cron aktiviert
201
202
  no_good_job_processes_found: Keine GoodJob-Prozesse gefunden.
202
203
  process: Verfahren
203
204
  schedulers: Planer
@@ -205,6 +206,9 @@ de:
205
206
  title: Prozesse
206
207
  updated: Aktualisiert
207
208
  shared:
209
+ boolean:
210
+ 'false': Nein
211
+ 'true': Ja
208
212
  error: Fehler
209
213
  filter:
210
214
  all: Alle
@@ -198,6 +198,7 @@ en:
198
198
  unit: ''
199
199
  processes:
200
200
  index:
201
+ cron_enabled: Cron enabled
201
202
  no_good_job_processes_found: No GoodJob processes found.
202
203
  process: Process
203
204
  schedulers: Schedulers
@@ -205,6 +206,9 @@ en:
205
206
  title: Processes
206
207
  updated: Updated
207
208
  shared:
209
+ boolean:
210
+ 'false': 'No'
211
+ 'true': 'Yes'
208
212
  error: Error
209
213
  filter:
210
214
  all: All
@@ -196,6 +196,7 @@ es:
196
196
  unit: ''
197
197
  processes:
198
198
  index:
199
+ cron_enabled: Cron habilitado
199
200
  no_good_job_processes_found: No hay procesos de GoodJob.
200
201
  process: Proceso
201
202
  schedulers: Schedulers
@@ -203,6 +204,9 @@ es:
203
204
  title: Procesos
204
205
  updated: Actualizado
205
206
  shared:
207
+ boolean:
208
+ 'false': 'No'
209
+ 'true': Sí
206
210
  error: Error
207
211
  filter:
208
212
  all: Todas
@@ -198,6 +198,7 @@ fr:
198
198
  unit: ''
199
199
  processes:
200
200
  index:
201
+ cron_enabled: Cron activé
201
202
  no_good_job_processes_found: Aucun processus GoodJob trouvé.
202
203
  process: Processus
203
204
  schedulers: Schedulers
@@ -205,6 +206,9 @@ fr:
205
206
  title: Processus
206
207
  updated: Mis à jour
207
208
  shared:
209
+ boolean:
210
+ 'false': Non
211
+ 'true': Oui
208
212
  error: Erreur
209
213
  filter:
210
214
  all: Tous
@@ -198,6 +198,7 @@ ja:
198
198
  unit: ''
199
199
  processes:
200
200
  index:
201
+ cron_enabled: Cron が有効になっている
201
202
  no_good_job_processes_found: GoodJobのプロセスが見つかりませんでした。
202
203
  process: プロセス
203
204
  schedulers: スケジューラー
@@ -205,6 +206,9 @@ ja:
205
206
  title: プロセス
206
207
  updated: 更新された
207
208
  shared:
209
+ boolean:
210
+ 'false': いいえ
211
+ 'true': はい
208
212
  error: エラー
209
213
  filter:
210
214
  all: 全て
@@ -198,6 +198,7 @@ nl:
198
198
  unit: ''
199
199
  processes:
200
200
  index:
201
+ cron_enabled: Cron ingeschakeld
201
202
  no_good_job_processes_found: Geen GoodJob-processen gevonden.
202
203
  process: Proces
203
204
  schedulers: Planners
@@ -205,6 +206,9 @@ nl:
205
206
  title: Processen
206
207
  updated: Bijgewerkt
207
208
  shared:
209
+ boolean:
210
+ 'false': Nee
211
+ 'true': Ja
208
212
  error: Fout
209
213
  filter:
210
214
  all: Alle
@@ -224,6 +224,7 @@ ru:
224
224
  unit: ''
225
225
  processes:
226
226
  index:
227
+ cron_enabled: Крон включен
227
228
  no_good_job_processes_found: Процессы GoodJob не найдены.
228
229
  process: Процесс
229
230
  schedulers: Планировщики
@@ -231,6 +232,9 @@ ru:
231
232
  title: Процессы
232
233
  updated: Обновлено
233
234
  shared:
235
+ boolean:
236
+ 'false': Нет
237
+ 'true': Да
234
238
  error: Ошибка
235
239
  filter:
236
240
  all: Все
@@ -198,6 +198,7 @@ tr:
198
198
  unit: ''
199
199
  processes:
200
200
  index:
201
+ cron_enabled: Cron etkin
201
202
  no_good_job_processes_found: GoodJob süreci bulunamadı.
202
203
  process: Süreç
203
204
  schedulers: Planlayıcılar
@@ -205,6 +206,9 @@ tr:
205
206
  title: Süreçler
206
207
  updated: Güncellenmiş
207
208
  shared:
209
+ boolean:
210
+ 'false': Hayır
211
+ 'true': Evet
208
212
  error: Hata
209
213
  filter:
210
214
  all: Tümü
@@ -224,6 +224,7 @@ uk:
224
224
  unit: ''
225
225
  processes:
226
226
  index:
227
+ cron_enabled: Cron увімкнено
227
228
  no_good_job_processes_found: Процеси GoodJob не знайдені.
228
229
  process: Процес
229
230
  schedulers: Планувальники
@@ -231,6 +232,9 @@ uk:
231
232
  title: Процеси
232
233
  updated: Оновлено
233
234
  shared:
235
+ boolean:
236
+ 'false': Ні
237
+ 'true': Так
234
238
  error: Помилка
235
239
  filter:
236
240
  all: Всі
@@ -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,40 +139,42 @@ 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.
167
- # @param timeout [nil, Numeric, Symbol] Seconds to wait for active threads.
171
+ # @param timeout [nil, Numeric, NONE] Seconds to wait for active threads.
168
172
  # * +nil+ trigger a shutdown but not wait for it to complete.
169
173
  # * +-1+ wait until the shutdown is complete.
170
174
  # * +0+ immediately shutdown and stop any threads.
171
175
  # * A positive number will wait that many seconds before stopping any remaining active threads.
172
176
  # @return [void]
173
- def shutdown(timeout: :default)
177
+ def shutdown(timeout: NONE)
174
178
  @capsule&.shutdown(timeout: timeout)
175
179
  @_async_started = false
176
180
  end
@@ -44,23 +44,23 @@ module GoodJob
44
44
  end
45
45
 
46
46
  # Shut down the thread pool executors.
47
- # @param timeout [nil, Numeric, Symbol] Seconds to wait for active threads.
47
+ # @param timeout [nil, Numeric, NONE] Seconds to wait for active threads.
48
48
  # * +-1+ will wait for all active threads to complete.
49
49
  # * +0+ will interrupt active threads.
50
50
  # * +N+ will wait at most N seconds and then interrupt active threads.
51
51
  # * +nil+ will trigger a shutdown but not wait for it to complete.
52
52
  # @return [void]
53
- def shutdown(timeout: :default)
54
- timeout = @configuration.shutdown_timeout if timeout == :default
53
+ def shutdown(timeout: NONE)
54
+ timeout = @configuration.shutdown_timeout if timeout == NONE
55
55
  GoodJob._shutdown_all([@shared_executor, @notifier, @poller, @scheduler, @cron_manager].compact, timeout: timeout)
56
56
  @startable = false
57
57
  @running = false
58
58
  end
59
59
 
60
60
  # Shutdown and then start the capsule again.
61
- # @param timeout [Numeric, Symbol] Seconds to wait for active threads.
61
+ # @param timeout [Numeric, NONE] Seconds to wait for active threads.
62
62
  # @return [void]
63
- def restart(timeout: :default)
63
+ def restart(timeout: NONE)
64
64
  raise ArgumentError, "Capsule#restart cannot be called with a timeout of nil" if timeout.nil?
65
65
 
66
66
  shutdown(timeout: timeout)
data/lib/good_job/cli.rb CHANGED
@@ -17,6 +17,9 @@ module GoodJob
17
17
  # Requiring this loads the application's configuration and classes.
18
18
  RAILS_ENVIRONMENT_RB = File.expand_path("config/environment.rb")
19
19
 
20
+ # Number of seconds between checking shutdown conditions
21
+ SHUTDOWN_EVENT_TIMEOUT = 10
22
+
20
23
  class << self
21
24
  # Whether the CLI is running from the executable
22
25
  # @return [Boolean, nil]
@@ -106,14 +109,15 @@ module GoodJob
106
109
  probe_server.start
107
110
  end
108
111
 
109
- @stop_good_job_executable = false
112
+ require 'concurrent/atomic/event'
113
+ @stop_good_job_executable = Concurrent::Event.new
110
114
  %w[INT TERM].each do |signal|
111
- trap(signal) { @stop_good_job_executable = true }
115
+ trap(signal) { Thread.new { @stop_good_job_executable.set }.join }
112
116
  end
113
117
 
114
118
  Kernel.loop do
115
- sleep 0.1
116
- break if @stop_good_job_executable || capsule.shutdown?
119
+ @stop_good_job_executable.wait(SHUTDOWN_EVENT_TIMEOUT)
120
+ break if @stop_good_job_executable.set? || capsule.shutdown?
117
121
  end
118
122
 
119
123
  systemd.stop do
@@ -2,7 +2,7 @@
2
2
 
3
3
  module GoodJob
4
4
  # GoodJob gem version.
5
- VERSION = '3.19.4'
5
+ VERSION = '3.21.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
@@ -42,6 +42,10 @@ require "good_job/systemd_service"
42
42
  module GoodJob
43
43
  include GoodJob::Dependencies
44
44
 
45
+ # Default, null, blank value placeholder.
46
+ NONE = Module.new.freeze
47
+
48
+ # Default logger for GoodJob; overridden by Rails.logger in Railtie.
45
49
  DEFAULT_LOGGER = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new($stdout))
46
50
 
47
51
  # @!attribute [rw] active_record_parent_class
@@ -233,13 +237,19 @@ module GoodJob
233
237
  # This is primarily intended for usage in a test environment.
234
238
  # Unhandled job errors will be raised.
235
239
  # @param queue_string [String] Queues to execute jobs from
240
+ # @param limit [Integer, nil] Maximum number of iterations for the loop
236
241
  # @return [void]
237
- def self.perform_inline(queue_string = "*")
242
+ def self.perform_inline(queue_string = "*", limit: nil)
238
243
  job_performer = JobPerformer.new(queue_string)
244
+ iteration = 0
239
245
  loop do
240
- result = job_performer.next
246
+ break if limit && iteration >= limit
247
+
248
+ result = Rails.application.reloader.wrap { job_performer.next }
241
249
  break unless result
242
250
  raise result.unhandled_error if result.unhandled_error
251
+
252
+ iteration += 1
243
253
  end
244
254
  end
245
255
 
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.4
4
+ version: 3.21.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-10-04 00:00:00.000000000 Z
11
+ date: 2023-11-06 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'