good_job 3.16.2 → 3.16.4
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 +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +44 -16
- data/app/models/concerns/good_job/{lockable.rb → advisory_lockable.rb} +1 -1
- data/app/models/concerns/good_job/error_events.rb +26 -9
- data/app/models/good_job/base_execution.rb +26 -0
- data/app/models/good_job/batch.rb +2 -2
- data/app/models/good_job/batch_record.rb +1 -1
- data/app/models/good_job/execution.rb +1 -26
- data/app/models/good_job/job.rb +6 -7
- data/app/models/good_job/process.rb +5 -1
- data/lib/good_job/adapter.rb +9 -1
- data/lib/good_job/capsule.rb +5 -4
- data/lib/good_job/configuration.rb +2 -5
- data/lib/good_job/cron_manager.rb +3 -2
- data/lib/good_job/notifier.rb +59 -44
- data/lib/good_job/shared_executor.rb +69 -0
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +13 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c4853b6c3598f46587b44f56dc6c59f4694753184da2d75d84cc87b8e901a3c3
|
|
4
|
+
data.tar.gz: 8086b714bc3a69a9260c36ef2b8295fc86ed6797eace2d8d8a4724ba509a7e65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5962d1992f8e283a6ca8349c1a9cf7d986bce54a2dc5f0abc09c1940ec97c7bc24af022eaa8e5979bcde395a4876741f34065cc24dfd1c88cb08a2a97baae77d
|
|
7
|
+
data.tar.gz: a1407ceaa0930ecf591fbf330e310124f6aaf9fd5ffc05aa579ae6176b8346dc004ba2ce2dc8d15b57efe9c1b07e68a248408cf0bec501bf5f84e586903d5c73
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v3.16.4](https://github.com/bensheldon/good_job/tree/v3.16.4) (2023-07-30)
|
|
4
|
+
|
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.16.3...v3.16.4)
|
|
6
|
+
|
|
7
|
+
**Implemented enhancements:**
|
|
8
|
+
|
|
9
|
+
- Add database\_connection\_pool stats to `GoodJob::Process.current_state` [\#1019](https://github.com/bensheldon/good_job/pull/1019) ([dixpac](https://github.com/dixpac))
|
|
10
|
+
|
|
11
|
+
**Fixed bugs:**
|
|
12
|
+
|
|
13
|
+
- Move `Execution#active_job` to BaseExecution to share with Job; refactor `Batch#active_jobs` to use job directly [\#1022](https://github.com/bensheldon/good_job/pull/1022) ([bensheldon](https://github.com/bensheldon))
|
|
14
|
+
|
|
15
|
+
**Closed issues:**
|
|
16
|
+
|
|
17
|
+
- Notifier errored: ArgumentError: wrong number of arguments \(given 1, expected 0\) [\#1016](https://github.com/bensheldon/good_job/issues/1016)
|
|
18
|
+
- Understanding Database Connections and Cron [\#1013](https://github.com/bensheldon/good_job/issues/1013)
|
|
19
|
+
- Experiencing various database exceptions with Rails 7.1 [\#796](https://github.com/bensheldon/good_job/issues/796)
|
|
20
|
+
|
|
21
|
+
**Merged pull requests:**
|
|
22
|
+
|
|
23
|
+
- Refactor Notifier to ensure \#restart is threadsafe [\#1021](https://github.com/bensheldon/good_job/pull/1021) ([bensheldon](https://github.com/bensheldon))
|
|
24
|
+
- Notifier and CronManager share a 2-thread executor within the capsule [\#1018](https://github.com/bensheldon/good_job/pull/1018) ([bensheldon](https://github.com/bensheldon))
|
|
25
|
+
- Clarify database connections and recurring processes in README.md [\#1015](https://github.com/bensheldon/good_job/pull/1015) ([blumhardts](https://github.com/blumhardts))
|
|
26
|
+
- Deprecate `GoodJob::Lockable` and rename to `GoodJob::AdvisoryLockable` [\#1012](https://github.com/bensheldon/good_job/pull/1012) ([bensheldon](https://github.com/bensheldon))
|
|
27
|
+
|
|
28
|
+
## [v3.16.3](https://github.com/bensheldon/good_job/tree/v3.16.3) (2023-07-18)
|
|
29
|
+
|
|
30
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.16.2...v3.16.3)
|
|
31
|
+
|
|
32
|
+
**Fixed bugs:**
|
|
33
|
+
|
|
34
|
+
- Fix bulk enqueue for unmigrated 'error\_event'; add `GoodJob.migrated?` check method; use custom enum implementation [\#1011](https://github.com/bensheldon/good_job/pull/1011) ([bensheldon](https://github.com/bensheldon))
|
|
35
|
+
|
|
36
|
+
**Closed issues:**
|
|
37
|
+
|
|
38
|
+
- GoodJob::Bulk.enqueue not handling missing migrations [\#1010](https://github.com/bensheldon/good_job/issues/1010)
|
|
39
|
+
|
|
40
|
+
**Merged pull requests:**
|
|
41
|
+
|
|
42
|
+
- Move shared `BaseExecution` concerns into the base class. [\#1009](https://github.com/bensheldon/good_job/pull/1009) ([dixpac](https://github.com/dixpac))
|
|
43
|
+
|
|
3
44
|
## [v3.16.2](https://github.com/bensheldon/good_job/tree/v3.16.2) (2023-07-13)
|
|
4
45
|
|
|
5
46
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.16.1...v3.16.2)
|
data/README.md
CHANGED
|
@@ -59,8 +59,8 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla
|
|
|
59
59
|
- [Timeouts](#timeouts)
|
|
60
60
|
- [Optimize queues, threads, and processes](#optimize-queues-threads-and-processes)
|
|
61
61
|
- [Database connections](#database-connections)
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
- [Production setup](#production-setup)
|
|
63
|
+
- [Queue performance with Queue Select Limit](#queue-performance-with-queue-select-limit)
|
|
64
64
|
- [Execute jobs async / in-process](#execute-jobs-async--in-process)
|
|
65
65
|
- [Migrate to GoodJob from a different ActiveJob backend](#migrate-to-goodjob-from-a-different-activejob-backend)
|
|
66
66
|
- [Monitor and preserve worked jobs](#monitor-and-preserve-worked-jobs)
|
|
@@ -475,7 +475,7 @@ GoodJob's concurrency control strategy for `perform_limit` is "optimistic retry
|
|
|
475
475
|
|
|
476
476
|
GoodJob can enqueue jobs on a recurring basis that can be used as a replacement for cron.
|
|
477
477
|
|
|
478
|
-
Cron-style jobs
|
|
478
|
+
Cron-style jobs can be performed by any GoodJob process (e.g., CLI or `:async` execution mode) that has `config.good_job.enable_cron` set to `true`. That is, one or more job executor processes can be configured to perform recurring jobs.
|
|
479
479
|
|
|
480
480
|
GoodJob's cron uses unique indexes to ensure that only a single job is enqueued at the given time interval. In order for this to work, GoodJob must preserve cron-created job records; these records will be automatically deleted like any other preserved record.
|
|
481
481
|
|
|
@@ -484,7 +484,7 @@ Cron-format is parsed by the [`fugit`](https://github.com/floraison/fugit) gem,
|
|
|
484
484
|
```ruby
|
|
485
485
|
# config/environments/application.rb or a specific environment e.g. production.rb
|
|
486
486
|
|
|
487
|
-
# Enable cron in this process
|
|
487
|
+
# Enable cron in this process, e.g., only run on the first Heroku worker process
|
|
488
488
|
config.good_job.enable_cron = ENV['DYNO'] == 'worker.1' # or `true` or via $GOOD_JOB_ENABLE_CRON
|
|
489
489
|
|
|
490
490
|
# Configure cron with a hash that has a unique key for each recurring job
|
|
@@ -703,7 +703,9 @@ GoodJob follows semantic versioning, though updates may be encouraged through de
|
|
|
703
703
|
|
|
704
704
|
#### Upgrading minor versions
|
|
705
705
|
|
|
706
|
-
Upgrading between minor versions (e.g. v1.4 to v1.5) should not introduce breaking changes, but can introduce new deprecation warnings and database migration
|
|
706
|
+
Upgrading between minor versions (e.g. v1.4 to v1.5) should not introduce breaking changes, but can introduce new deprecation warnings and database migration warnings.
|
|
707
|
+
|
|
708
|
+
Database migrations introduced in minor releases are _not required_ to be applied until the next major release. If you would like apply newly introduced migrations immediately, assert `GoodJob.migrated?` in your application's test suite.
|
|
707
709
|
|
|
708
710
|
To perform upgrades to the GoodJob database tables:
|
|
709
711
|
|
|
@@ -952,24 +954,50 @@ Keep in mind, queue operations and management is an advanced discipline. This st
|
|
|
952
954
|
|
|
953
955
|
### Database connections
|
|
954
956
|
|
|
955
|
-
|
|
957
|
+
GoodJob job executor processes require the following database connections:
|
|
958
|
+
|
|
959
|
+
- 1 connection per execution pool thread. E.g., `--queues=mice:2;elephants:1` is 3 threads and thus 3 connections. Pool size defaults to `--max-threads`.
|
|
960
|
+
- 2 additional connections that GoodJob uses for utility functionality (e.g. LISTEN/NOTIFY, cron, etc.)
|
|
961
|
+
- 1 connection per subthread, if your application makes multithreaded database queries (e.g. `load_async`) within a job.
|
|
962
|
+
|
|
963
|
+
The executor process will not crash if the connections pool is exhausted, instead it will report an exception (eg. `ActiveRecord::ConnectionTimeoutError`).
|
|
964
|
+
|
|
965
|
+
When GoodJob runs in `:inline` mode (in Rails' test environment, by default), the default database pool configuration works.
|
|
966
|
+
|
|
967
|
+
```yml
|
|
968
|
+
# config/database.yml
|
|
969
|
+
|
|
970
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
When GoodJob runs in `:async` mode (in Rails's development environment, by default), the following database pool configuration works, where:
|
|
974
|
+
|
|
975
|
+
- `ENV.fetch("RAILS_MAX_THREADS", 5)` is the number of threads used by the web server
|
|
976
|
+
- `1` is the number of connections used by the job listener
|
|
977
|
+
- `2` is the number of connections used by the cron scheduler and executor
|
|
978
|
+
- `ENV.fetch("GOOD_JOB_MAX_THREADS", 5)` is the number of threads used to perform jobs
|
|
956
979
|
|
|
957
980
|
```yaml
|
|
958
981
|
# config/database.yml
|
|
959
|
-
|
|
982
|
+
|
|
983
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5).to_i + 1 + 2 + ENV.fetch("GOOD_JOB_MAX_THREADS", 5).to_i %>
|
|
960
984
|
```
|
|
961
985
|
|
|
962
|
-
|
|
986
|
+
When GoodJob runs in `:external` mode (in Rails' production environment, by default), the following database pool configurations work for web servers and worker processes, respectively.
|
|
963
987
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
988
|
+
```yaml
|
|
989
|
+
# config/database.yml
|
|
990
|
+
|
|
991
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
|
|
992
|
+
```
|
|
969
993
|
|
|
970
|
-
|
|
994
|
+
```yaml
|
|
995
|
+
# config/database.yml
|
|
996
|
+
|
|
997
|
+
pool: <%= 1 + 2 + ENV.fetch("GOOD_JOB_MAX_THREADS", 5).to_i %>
|
|
998
|
+
```
|
|
971
999
|
|
|
972
|
-
|
|
1000
|
+
### Production setup
|
|
973
1001
|
|
|
974
1002
|
When running GoodJob in a production environment, you should be mindful of:
|
|
975
1003
|
|
|
@@ -984,7 +1012,7 @@ The recommended way to monitor the queue in production is:
|
|
|
984
1012
|
- keep an eye on the number of jobs in the queue (abnormal high number of unscheduled jobs means the queue could be underperforming)
|
|
985
1013
|
- consider performance monitoring services which support the built-in Rails instrumentation (eg. Sentry, Skylight, etc.)
|
|
986
1014
|
|
|
987
|
-
|
|
1015
|
+
### Queue performance with Queue Select Limit
|
|
988
1016
|
|
|
989
1017
|
GoodJob’s advisory locking strategy uses a materialized CTE (Common Table Expression). This strategy can be non-performant when querying a very large queue of executable jobs (100,000+) because the database query must materialize all executable jobs before acquiring an advisory lock.
|
|
990
1018
|
|
|
@@ -14,15 +14,32 @@ module GoodJob
|
|
|
14
14
|
ERROR_EVENT_DISCARDED = 'discarded',
|
|
15
15
|
].freeze
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
ERROR_EVENT_ENUMS = {
|
|
18
|
+
ERROR_EVENT_INTERRUPTED => 0,
|
|
19
|
+
ERROR_EVENT_UNHANDLED => 1,
|
|
20
|
+
ERROR_EVENT_HANDLED => 2,
|
|
21
|
+
ERROR_EVENT_RETRIED => 3,
|
|
22
|
+
ERROR_EVENT_RETRY_STOPPED => 4,
|
|
23
|
+
ERROR_EVENT_DISCARDED => 5,
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
# TODO: GoodJob v4 can make this an `enum` once migrations are guaranteed.
|
|
27
|
+
def error_event
|
|
28
|
+
return unless self.class.columns_hash['error_event']
|
|
29
|
+
|
|
30
|
+
enum = super
|
|
31
|
+
return unless enum
|
|
32
|
+
|
|
33
|
+
ERROR_EVENT_ENUMS.key(enum)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def error_event=(event)
|
|
37
|
+
return unless self.class.columns_hash['error_event']
|
|
38
|
+
|
|
39
|
+
enum = ERROR_EVENT_ENUMS[event]
|
|
40
|
+
raise(ArgumentError, "Invalid error_event: #{event}") if event && !enum
|
|
41
|
+
|
|
42
|
+
super(enum)
|
|
26
43
|
end
|
|
27
44
|
end
|
|
28
45
|
end
|
|
@@ -4,7 +4,10 @@ module GoodJob
|
|
|
4
4
|
# ActiveRecord model to share behavior between {Job} and {Execution} models
|
|
5
5
|
# which both read out of the same table.
|
|
6
6
|
class BaseExecution < BaseRecord
|
|
7
|
+
include AdvisoryLockable
|
|
7
8
|
include ErrorEvents
|
|
9
|
+
include Filterable
|
|
10
|
+
include Reportable
|
|
8
11
|
|
|
9
12
|
self.table_name = 'good_jobs'
|
|
10
13
|
|
|
@@ -57,5 +60,28 @@ module GoodJob
|
|
|
57
60
|
def discrete?
|
|
58
61
|
self.class.discrete_support? && is_discrete?
|
|
59
62
|
end
|
|
63
|
+
|
|
64
|
+
# Build an ActiveJob instance and deserialize the arguments, using `#active_job_data`.
|
|
65
|
+
#
|
|
66
|
+
# @param ignore_deserialization_errors [Boolean]
|
|
67
|
+
# Whether to ignore ActiveJob::DeserializationError when deserializing the arguments.
|
|
68
|
+
# This is most useful if you aren't planning to use the arguments directly.
|
|
69
|
+
def active_job(ignore_deserialization_errors: false)
|
|
70
|
+
ActiveJob::Base.deserialize(active_job_data).tap do |aj|
|
|
71
|
+
aj.send(:deserialize_arguments_if_needed)
|
|
72
|
+
rescue ActiveJob::DeserializationError
|
|
73
|
+
raise unless ignore_deserialization_errors
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def active_job_data
|
|
80
|
+
serialized_params.deep_dup
|
|
81
|
+
.tap do |job_data|
|
|
82
|
+
job_data["provider_job_id"] = id
|
|
83
|
+
job_data["good_job_concurrency_key"] = concurrency_key if concurrency_key
|
|
84
|
+
end
|
|
85
|
+
end
|
|
60
86
|
end
|
|
61
87
|
end
|
|
@@ -124,11 +124,11 @@ module GoodJob
|
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
def active_jobs
|
|
127
|
-
record.jobs.map(&:
|
|
127
|
+
record.jobs.map(&:active_job)
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
def callback_active_jobs
|
|
131
|
-
record.callback_jobs.map(&:
|
|
131
|
+
record.callback_jobs.map(&:active_job)
|
|
132
132
|
end
|
|
133
133
|
|
|
134
134
|
def assign_properties(properties)
|
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
module GoodJob
|
|
4
4
|
# ActiveRecord model that represents an +ActiveJob+ job.
|
|
5
5
|
class Execution < BaseExecution
|
|
6
|
-
include Lockable
|
|
7
|
-
include Filterable
|
|
8
|
-
include Reportable
|
|
9
|
-
|
|
10
6
|
# Raised if something attempts to execute a previously completed Execution again.
|
|
11
7
|
PreviouslyPerformedError = Class.new(StandardError)
|
|
12
8
|
|
|
@@ -384,7 +380,7 @@ module GoodJob
|
|
|
384
380
|
error: interrupt_error_string,
|
|
385
381
|
finished_at: Time.current,
|
|
386
382
|
}
|
|
387
|
-
discrete_execution_attrs[:error_event] = GoodJob::
|
|
383
|
+
discrete_execution_attrs[:error_event] = GoodJob::ErrorEvents::ERROR_EVENT_ENUMS[GoodJob::ErrorEvents::ERROR_EVENT_INTERRUPTED] if self.class.error_event_migrated?
|
|
388
384
|
discrete_executions.where(finished_at: nil).where.not(performed_at: nil).update_all(discrete_execution_attrs) # rubocop:disable Rails/SkipsModelValidations
|
|
389
385
|
end
|
|
390
386
|
end
|
|
@@ -512,19 +508,6 @@ module GoodJob
|
|
|
512
508
|
self.scheduled_at ||= current_time
|
|
513
509
|
end
|
|
514
510
|
|
|
515
|
-
# Build an ActiveJob instance and deserialize the arguments, using `#active_job_data`.
|
|
516
|
-
#
|
|
517
|
-
# @param ignore_deserialization_errors [Boolean]
|
|
518
|
-
# Whether to ignore ActiveJob::DeserializationError when deserializing the arguments.
|
|
519
|
-
# This is most useful if you aren't planning to use the arguments directly.
|
|
520
|
-
def active_job(ignore_deserialization_errors: false)
|
|
521
|
-
ActiveJob::Base.deserialize(active_job_data).tap do |aj|
|
|
522
|
-
aj.send(:deserialize_arguments_if_needed)
|
|
523
|
-
rescue ActiveJob::DeserializationError
|
|
524
|
-
raise unless ignore_deserialization_errors
|
|
525
|
-
end
|
|
526
|
-
end
|
|
527
|
-
|
|
528
511
|
# Return formatted serialized_params for display in the dashboard
|
|
529
512
|
# @return [Hash]
|
|
530
513
|
def display_serialized_params
|
|
@@ -569,14 +552,6 @@ module GoodJob
|
|
|
569
552
|
|
|
570
553
|
private
|
|
571
554
|
|
|
572
|
-
def active_job_data
|
|
573
|
-
serialized_params.deep_dup
|
|
574
|
-
.tap do |job_data|
|
|
575
|
-
job_data["provider_job_id"] = id
|
|
576
|
-
job_data["good_job_concurrency_key"] = concurrency_key if concurrency_key
|
|
577
|
-
end
|
|
578
|
-
end
|
|
579
|
-
|
|
580
555
|
def reset_batch_values(&block)
|
|
581
556
|
GoodJob::Batch.within_thread(batch_id: nil, batch_callback_id: nil, &block)
|
|
582
557
|
end
|
data/app/models/good_job/job.rb
CHANGED
|
@@ -7,10 +7,6 @@ module GoodJob
|
|
|
7
7
|
# A single row from the +good_jobs+ table of executions is fetched to represent a Job.
|
|
8
8
|
#
|
|
9
9
|
class Job < BaseExecution
|
|
10
|
-
include Filterable
|
|
11
|
-
include Lockable
|
|
12
|
-
include Reportable
|
|
13
|
-
|
|
14
10
|
# Raised when an inappropriate action is applied to a Job based on its state.
|
|
15
11
|
ActionForStateMismatchError = Class.new(StandardError)
|
|
16
12
|
# Raised when an action requires GoodJob to be the ActiveJob Queue Adapter but GoodJob is not.
|
|
@@ -228,9 +224,12 @@ module GoodJob
|
|
|
228
224
|
|
|
229
225
|
update_execution = proc do
|
|
230
226
|
execution.update(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
227
|
+
{
|
|
228
|
+
finished_at: Time.current,
|
|
229
|
+
error: GoodJob::Execution.format_error(job_error),
|
|
230
|
+
}.tap do |attrs|
|
|
231
|
+
attrs[:error_event] = ERROR_EVENT_DISCARDED if self.class.error_event_migrated?
|
|
232
|
+
end
|
|
234
233
|
)
|
|
235
234
|
end
|
|
236
235
|
|
|
@@ -5,8 +5,8 @@ require 'socket'
|
|
|
5
5
|
module GoodJob # :nodoc:
|
|
6
6
|
# ActiveRecord model that represents an GoodJob process (either async or CLI).
|
|
7
7
|
class Process < BaseRecord
|
|
8
|
+
include AdvisoryLockable
|
|
8
9
|
include AssignableConnection
|
|
9
|
-
include Lockable
|
|
10
10
|
|
|
11
11
|
# Interval until the process record being updated
|
|
12
12
|
STALE_INTERVAL = 30.seconds
|
|
@@ -63,6 +63,10 @@ module GoodJob # :nodoc:
|
|
|
63
63
|
cron_enabled: GoodJob.configuration.enable_cron?,
|
|
64
64
|
total_succeeded_executions_count: GoodJob::Scheduler.instances.sum { |scheduler| scheduler.stats.fetch(:succeeded_executions_count) },
|
|
65
65
|
total_errored_executions_count: GoodJob::Scheduler.instances.sum { |scheduler| scheduler.stats.fetch(:errored_executions_count) },
|
|
66
|
+
database_connection_pool: {
|
|
67
|
+
size: connection_pool.size,
|
|
68
|
+
active: connection_pool.connections.count(&:in_use?),
|
|
69
|
+
},
|
|
66
70
|
}
|
|
67
71
|
end
|
|
68
72
|
|
data/lib/good_job/adapter.rb
CHANGED
|
@@ -64,7 +64,15 @@ module GoodJob
|
|
|
64
64
|
|
|
65
65
|
inline_executions = []
|
|
66
66
|
GoodJob::Execution.transaction(requires_new: true, joinable: false) do
|
|
67
|
-
|
|
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')
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
results = GoodJob::Execution.insert_all(execution_attributes, returning: %w[id active_job_id]) # rubocop:disable Rails/SkipsModelValidations
|
|
68
76
|
|
|
69
77
|
job_id_to_provider_job_id = results.each_with_object({}) { |result, hash| hash[result['active_job_id']] = result['id'] }
|
|
70
78
|
active_jobs.each do |active_job|
|
data/lib/good_job/capsule.rb
CHANGED
|
@@ -29,13 +29,14 @@ module GoodJob
|
|
|
29
29
|
@mutex.synchronize do
|
|
30
30
|
return unless startable?(force: force)
|
|
31
31
|
|
|
32
|
-
@
|
|
32
|
+
@shared_executor = GoodJob::SharedExecutor.new
|
|
33
|
+
@notifier = GoodJob::Notifier.new(enable_listening: @configuration.enable_listen_notify, executor: @shared_executor.executor)
|
|
33
34
|
@poller = GoodJob::Poller.new(poll_interval: @configuration.poll_interval)
|
|
34
35
|
@scheduler = GoodJob::Scheduler.from_configuration(@configuration, warm_cache_on_initialize: true)
|
|
35
36
|
@notifier.recipients << [@scheduler, :create_thread]
|
|
36
37
|
@poller.recipients << [@scheduler, :create_thread]
|
|
37
38
|
|
|
38
|
-
@cron_manager = GoodJob::CronManager.new(@configuration.cron_entries, start_on_initialize: true) if @configuration.enable_cron?
|
|
39
|
+
@cron_manager = GoodJob::CronManager.new(@configuration.cron_entries, start_on_initialize: true, executor: @shared_executor.executor) if @configuration.enable_cron?
|
|
39
40
|
|
|
40
41
|
@startable = false
|
|
41
42
|
@running = true
|
|
@@ -51,7 +52,7 @@ module GoodJob
|
|
|
51
52
|
# @return [void]
|
|
52
53
|
def shutdown(timeout: :default)
|
|
53
54
|
timeout = @configuration.shutdown_timeout if timeout == :default
|
|
54
|
-
GoodJob._shutdown_all([@notifier, @poller, @scheduler, @cron_manager].compact, timeout: timeout)
|
|
55
|
+
GoodJob._shutdown_all([@shared_executor, @notifier, @poller, @scheduler, @cron_manager].compact, timeout: timeout)
|
|
55
56
|
@startable = false
|
|
56
57
|
@running = false
|
|
57
58
|
end
|
|
@@ -73,7 +74,7 @@ module GoodJob
|
|
|
73
74
|
|
|
74
75
|
# @return [Boolean] Whether the capsule has been shutdown.
|
|
75
76
|
def shutdown?
|
|
76
|
-
[@notifier, @poller, @scheduler, @cron_manager].compact.all?(&:shutdown?)
|
|
77
|
+
[@shared_executor, @notifier, @poller, @scheduler, @cron_manager].compact.all?(&:shutdown?)
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
# Creates an execution thread(s) with the given attributes.
|
|
@@ -51,13 +51,10 @@ module GoodJob
|
|
|
51
51
|
# @param warn [Boolean] whether to print a warning when over the limit
|
|
52
52
|
# @return [Integer]
|
|
53
53
|
def self.total_estimated_threads(warn: false)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
cron_threads = configuration.enable_cron? ? 2 : 0
|
|
57
|
-
notifier_threads = 1
|
|
54
|
+
utility_threads = GoodJob::SharedExecutor::MAX_THREADS
|
|
58
55
|
scheduler_threads = GoodJob::Scheduler.instances.sum { |scheduler| scheduler.stats[:max_threads] }
|
|
59
56
|
|
|
60
|
-
good_job_threads =
|
|
57
|
+
good_job_threads = utility_threads + scheduler_threads
|
|
61
58
|
puma_threads = (Puma::Server.current&.max_threads if defined?(Puma::Server)) || 0
|
|
62
59
|
|
|
63
60
|
total_threads = good_job_threads + puma_threads
|
|
@@ -31,7 +31,8 @@ module GoodJob # :nodoc:
|
|
|
31
31
|
|
|
32
32
|
# @param cron_entries [Array<CronEntry>]
|
|
33
33
|
# @param start_on_initialize [Boolean]
|
|
34
|
-
def initialize(cron_entries = [], start_on_initialize: false)
|
|
34
|
+
def initialize(cron_entries = [], start_on_initialize: false, executor: Concurrent.global_io_executor)
|
|
35
|
+
@executor = executor
|
|
35
36
|
@running = false
|
|
36
37
|
@cron_entries = cron_entries
|
|
37
38
|
@tasks = Concurrent::Hash.new
|
|
@@ -84,7 +85,7 @@ module GoodJob # :nodoc:
|
|
|
84
85
|
def create_task(cron_entry)
|
|
85
86
|
cron_at = cron_entry.next_at
|
|
86
87
|
delay = [(cron_at - Time.current).to_f, 0].max
|
|
87
|
-
future = Concurrent::ScheduledTask.new(delay, args: [self, cron_entry, cron_at]) do |thr_scheduler, thr_cron_entry, thr_cron_at|
|
|
88
|
+
future = Concurrent::ScheduledTask.new(delay, args: [self, cron_entry, cron_at], executor: @executor) do |thr_scheduler, thr_cron_entry, thr_cron_at|
|
|
88
89
|
# Re-schedule the next cron task before executing the current task
|
|
89
90
|
thr_scheduler.create_task(thr_cron_entry)
|
|
90
91
|
|
data/lib/good_job/notifier.rb
CHANGED
|
@@ -20,16 +20,6 @@ module GoodJob # :nodoc:
|
|
|
20
20
|
|
|
21
21
|
# Default Postgres channel for LISTEN/NOTIFY
|
|
22
22
|
CHANNEL = 'good_job'
|
|
23
|
-
# Defaults for instance of Concurrent::ThreadPoolExecutor
|
|
24
|
-
EXECUTOR_OPTIONS = {
|
|
25
|
-
name: name,
|
|
26
|
-
min_threads: 0,
|
|
27
|
-
max_threads: 1,
|
|
28
|
-
auto_terminate: true,
|
|
29
|
-
idletime: 60,
|
|
30
|
-
max_queue: 1,
|
|
31
|
-
fallback_policy: :discard,
|
|
32
|
-
}.freeze
|
|
33
23
|
# Seconds to block while LISTENing for a message
|
|
34
24
|
WAIT_INTERVAL = 1
|
|
35
25
|
# Seconds to wait if database cannot be connected to
|
|
@@ -70,19 +60,30 @@ module GoodJob # :nodoc:
|
|
|
70
60
|
|
|
71
61
|
# @param recipients [Array<#call, Array(Object, Symbol)>]
|
|
72
62
|
# @param enable_listening [true, false]
|
|
73
|
-
|
|
63
|
+
# @param executor [Concurrent::ExecutorService]
|
|
64
|
+
def initialize(*recipients, enable_listening: true, executor: Concurrent.global_io_executor)
|
|
74
65
|
@recipients = Concurrent::Array.new(recipients)
|
|
66
|
+
@enable_listening = enable_listening
|
|
67
|
+
@executor = executor
|
|
68
|
+
|
|
69
|
+
@mutex = Mutex.new
|
|
70
|
+
@shutdown_event = Concurrent::Event.new.tap(&:set)
|
|
71
|
+
@running = Concurrent::AtomicBoolean.new(false)
|
|
75
72
|
@connected = Concurrent::AtomicBoolean.new(false)
|
|
76
73
|
@listening = Concurrent::AtomicBoolean.new(false)
|
|
77
74
|
@connection_errors_count = Concurrent::AtomicFixnum.new(0)
|
|
78
75
|
@connection_errors_reported = Concurrent::AtomicBoolean.new(false)
|
|
79
76
|
@enable_listening = enable_listening
|
|
77
|
+
@task = nil
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
listen
|
|
79
|
+
start
|
|
83
80
|
self.class.instances << self
|
|
84
81
|
end
|
|
85
82
|
|
|
83
|
+
def running?
|
|
84
|
+
@executor.running? && @running.true?
|
|
85
|
+
end
|
|
86
|
+
|
|
86
87
|
# Tests whether the notifier is active and has acquired a dedicated database connection.
|
|
87
88
|
# @return [true, false, nil]
|
|
88
89
|
def connected?
|
|
@@ -95,15 +96,9 @@ module GoodJob # :nodoc:
|
|
|
95
96
|
@listening.true?
|
|
96
97
|
end
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
delegate :running?, to: :executor, allow_nil: true
|
|
102
|
-
|
|
103
|
-
# Tests whether the scheduler is shutdown.
|
|
104
|
-
# @!method shutdown?
|
|
105
|
-
# @return [true, false, nil]
|
|
106
|
-
delegate :shutdown?, to: :executor, allow_nil: true
|
|
99
|
+
def shutdown?
|
|
100
|
+
@shutdown_event.set?
|
|
101
|
+
end
|
|
107
102
|
|
|
108
103
|
# Shut down the notifier.
|
|
109
104
|
# This stops the background LISTENing thread.
|
|
@@ -111,17 +106,21 @@ module GoodJob # :nodoc:
|
|
|
111
106
|
# @param timeout [Numeric, nil] Seconds to wait for active threads.
|
|
112
107
|
# * +nil+, the scheduler will trigger a shutdown but not wait for it to complete.
|
|
113
108
|
# * +-1+, the scheduler will wait until the shutdown is complete.
|
|
114
|
-
# * +0+, the scheduler will immediately shutdown and stop any threads.
|
|
115
109
|
# * A positive number will wait that many seconds before stopping any remaining active threads.
|
|
116
110
|
# @return [void]
|
|
117
111
|
def shutdown(timeout: -1)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
112
|
+
synchronize do
|
|
113
|
+
@running.make_false
|
|
114
|
+
|
|
115
|
+
if @executor.shutdown? || @task&.complete?
|
|
116
|
+
# clean up in the even the executor is killed
|
|
117
|
+
@connected.make_false
|
|
118
|
+
@listening.make_false
|
|
119
|
+
@shutdown_event.set
|
|
120
|
+
else
|
|
121
|
+
@shutdown_event.wait(timeout == -1 ? nil : timeout) unless timeout.nil?
|
|
122
|
+
end
|
|
123
|
+
@shutdown_event.set?
|
|
125
124
|
end
|
|
126
125
|
end
|
|
127
126
|
|
|
@@ -130,9 +129,10 @@ module GoodJob # :nodoc:
|
|
|
130
129
|
# @param timeout [nil, Numeric] Seconds to wait; shares same values as {#shutdown}.
|
|
131
130
|
# @return [void]
|
|
132
131
|
def restart(timeout: -1)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
synchronize do
|
|
133
|
+
shutdown(timeout: timeout) unless @shutdown_event.set?
|
|
134
|
+
start
|
|
135
|
+
end
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
# Invoked on completion of ThreadPoolExecutor task
|
|
@@ -160,21 +160,27 @@ module GoodJob # :nodoc:
|
|
|
160
160
|
end
|
|
161
161
|
end
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
if @running.true?
|
|
164
|
+
create_listen_task(delay: connection_error ? RECONNECT_INTERVAL : 0)
|
|
165
|
+
else
|
|
166
|
+
@shutdown_event.set
|
|
167
|
+
end
|
|
166
168
|
end
|
|
167
169
|
|
|
168
170
|
private
|
|
169
171
|
|
|
170
|
-
|
|
172
|
+
def start
|
|
173
|
+
synchronize do
|
|
174
|
+
return if @running.true?
|
|
171
175
|
|
|
172
|
-
|
|
173
|
-
|
|
176
|
+
@running.make_true
|
|
177
|
+
@shutdown_event.reset
|
|
178
|
+
create_listen_task(delay: 0)
|
|
179
|
+
end
|
|
174
180
|
end
|
|
175
181
|
|
|
176
|
-
def
|
|
177
|
-
|
|
182
|
+
def create_listen_task(delay: 0)
|
|
183
|
+
@task = Concurrent::ScheduledTask.new(delay, args: [@recipients, @running, @executor, @enable_listening, @listening], executor: @executor) do |thr_recipients, thr_running, thr_executor, thr_enable_listening, thr_listening|
|
|
178
184
|
with_connection do
|
|
179
185
|
begin
|
|
180
186
|
Rails.application.executor.wrap do
|
|
@@ -188,7 +194,7 @@ module GoodJob # :nodoc:
|
|
|
188
194
|
end
|
|
189
195
|
end
|
|
190
196
|
|
|
191
|
-
while thr_executor.running?
|
|
197
|
+
while thr_executor.running? && thr_running.true?
|
|
192
198
|
run_callbacks :tick do
|
|
193
199
|
wait_for_notify do |channel, payload|
|
|
194
200
|
next unless channel == CHANNEL
|
|
@@ -219,8 +225,9 @@ module GoodJob # :nodoc:
|
|
|
219
225
|
end
|
|
220
226
|
end
|
|
221
227
|
|
|
222
|
-
|
|
223
|
-
|
|
228
|
+
@task.add_observer(self, :listen_observer)
|
|
229
|
+
@task.execute
|
|
230
|
+
@task
|
|
224
231
|
end
|
|
225
232
|
|
|
226
233
|
def with_connection
|
|
@@ -263,5 +270,13 @@ module GoodJob # :nodoc:
|
|
|
263
270
|
@connection_errors_count.value = 0
|
|
264
271
|
@connection_errors_reported.make_false
|
|
265
272
|
end
|
|
273
|
+
|
|
274
|
+
def synchronize(&block)
|
|
275
|
+
if @mutex.owned?
|
|
276
|
+
yield
|
|
277
|
+
else
|
|
278
|
+
@mutex.synchronize(&block)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
266
281
|
end
|
|
267
282
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GoodJob
|
|
4
|
+
class SharedExecutor
|
|
5
|
+
MAX_THREADS = 2
|
|
6
|
+
|
|
7
|
+
# @!attribute [r] instances
|
|
8
|
+
# @!scope class
|
|
9
|
+
# List of all instantiated SharedExecutor in the current process.
|
|
10
|
+
# @return [Array<GoodJob::SharedExecutor>, nil]
|
|
11
|
+
cattr_reader :instances, default: Concurrent::Array.new, instance_reader: false
|
|
12
|
+
|
|
13
|
+
attr_reader :executor
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
self.class.instances << self
|
|
17
|
+
create_executor
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def running?
|
|
21
|
+
@executor&.running?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def shutdown?
|
|
25
|
+
if @executor
|
|
26
|
+
@executor.shutdown?
|
|
27
|
+
else
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Shut down the SharedExecutor.
|
|
33
|
+
# Use {#shutdown?} to determine whether threads have stopped.
|
|
34
|
+
# @param timeout [Numeric, nil] Seconds to wait for active threads.
|
|
35
|
+
# * +nil+, the scheduler will trigger a shutdown but not wait for it to complete.
|
|
36
|
+
# * +-1+, the scheduler will wait until the shutdown is complete.
|
|
37
|
+
# * +0+, the scheduler will immediately shutdown and stop any threads.
|
|
38
|
+
# * A positive number will wait that many seconds before stopping any remaining active threads.
|
|
39
|
+
# @return [void]
|
|
40
|
+
def shutdown(timeout: -1)
|
|
41
|
+
return if @executor.nil? || @executor.shutdown?
|
|
42
|
+
|
|
43
|
+
@executor.shutdown if @executor.running?
|
|
44
|
+
|
|
45
|
+
if @executor.shuttingdown? && timeout # rubocop:disable Style/GuardClause
|
|
46
|
+
executor_wait = timeout.negative? ? nil : timeout
|
|
47
|
+
@executor.kill unless @executor.wait_for_termination(executor_wait)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def restart(timeout: -1)
|
|
52
|
+
shutdown(timeout: timeout) if running?
|
|
53
|
+
create_executor
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def create_executor
|
|
59
|
+
@executor = Concurrent::ThreadPoolExecutor.new(
|
|
60
|
+
min_threads: 0,
|
|
61
|
+
max_threads: MAX_THREADS,
|
|
62
|
+
auto_terminate: true,
|
|
63
|
+
idletime: 60,
|
|
64
|
+
max_queue: 0,
|
|
65
|
+
fallback_policy: :discard
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/good_job/version.rb
CHANGED
data/lib/good_job.rb
CHANGED
|
@@ -31,6 +31,7 @@ require "good_job/notifier"
|
|
|
31
31
|
require "good_job/poller"
|
|
32
32
|
require "good_job/probe_server"
|
|
33
33
|
require "good_job/scheduler"
|
|
34
|
+
require "good_job/shared_executor"
|
|
34
35
|
|
|
35
36
|
# GoodJob is a multithreaded, Postgres-based, ActiveJob backend for Ruby on Rails.
|
|
36
37
|
#
|
|
@@ -246,5 +247,17 @@ module GoodJob
|
|
|
246
247
|
end
|
|
247
248
|
end
|
|
248
249
|
|
|
250
|
+
include ActiveSupport::Deprecation::DeprecatedConstantAccessor
|
|
251
|
+
deprecate_constant :Lockable, 'GoodJob::AdvisoryLockable', deprecator: deprecator
|
|
252
|
+
|
|
253
|
+
# Whether all GoodJob migrations have been applied.
|
|
254
|
+
# For use in tests/CI to validate GoodJob is up-to-date.
|
|
255
|
+
# @return [Boolean]
|
|
256
|
+
def self.migrated?
|
|
257
|
+
# Always update with the most recent migration check
|
|
258
|
+
GoodJob::Execution.reset_column_information
|
|
259
|
+
GoodJob::Execution.error_event_migrated?
|
|
260
|
+
end
|
|
261
|
+
|
|
249
262
|
ActiveSupport.run_load_hooks(:good_job, self)
|
|
250
263
|
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.16.
|
|
4
|
+
version: 3.16.4
|
|
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-07-
|
|
11
|
+
date: 2023-07-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activejob
|
|
@@ -343,9 +343,9 @@ files:
|
|
|
343
343
|
- app/frontend/good_job/vendor/rails_ujs.js
|
|
344
344
|
- app/frontend/good_job/vendor/stimulus.js
|
|
345
345
|
- app/helpers/good_job/application_helper.rb
|
|
346
|
+
- app/models/concerns/good_job/advisory_lockable.rb
|
|
346
347
|
- app/models/concerns/good_job/error_events.rb
|
|
347
348
|
- app/models/concerns/good_job/filterable.rb
|
|
348
|
-
- app/models/concerns/good_job/lockable.rb
|
|
349
349
|
- app/models/concerns/good_job/reportable.rb
|
|
350
350
|
- app/models/good_job/active_record_parent_class.rb
|
|
351
351
|
- app/models/good_job/base_execution.rb
|
|
@@ -437,6 +437,7 @@ files:
|
|
|
437
437
|
- lib/good_job/poller.rb
|
|
438
438
|
- lib/good_job/probe_server.rb
|
|
439
439
|
- lib/good_job/scheduler.rb
|
|
440
|
+
- lib/good_job/shared_executor.rb
|
|
440
441
|
- lib/good_job/version.rb
|
|
441
442
|
homepage: https://github.com/bensheldon/good_job
|
|
442
443
|
licenses:
|