good_job 3.16.2 → 3.16.4
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|