good_job 3.16.3 → 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 +25 -0
- data/README.md +41 -15
- data/app/models/concerns/good_job/{lockable.rb → advisory_lockable.rb} +1 -1
- data/app/models/good_job/base_execution.rb +24 -1
- 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 +0 -21
- data/app/models/good_job/process.rb +5 -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 +4 -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,30 @@
|
|
|
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
|
+
|
|
3
28
|
## [v3.16.3](https://github.com/bensheldon/good_job/tree/v3.16.3) (2023-07-18)
|
|
4
29
|
|
|
5
30
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.16.2...v3.16.3)
|
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
|
|
@@ -954,24 +954,50 @@ Keep in mind, queue operations and management is an advanced discipline. This st
|
|
|
954
954
|
|
|
955
955
|
### Database connections
|
|
956
956
|
|
|
957
|
-
|
|
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
|
|
958
979
|
|
|
959
980
|
```yaml
|
|
960
981
|
# config/database.yml
|
|
961
|
-
|
|
982
|
+
|
|
983
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5).to_i + 1 + 2 + ENV.fetch("GOOD_JOB_MAX_THREADS", 5).to_i %>
|
|
962
984
|
```
|
|
963
985
|
|
|
964
|
-
|
|
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.
|
|
965
987
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
988
|
+
```yaml
|
|
989
|
+
# config/database.yml
|
|
990
|
+
|
|
991
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
|
|
992
|
+
```
|
|
971
993
|
|
|
972
|
-
|
|
994
|
+
```yaml
|
|
995
|
+
# config/database.yml
|
|
996
|
+
|
|
997
|
+
pool: <%= 1 + 2 + ENV.fetch("GOOD_JOB_MAX_THREADS", 5).to_i %>
|
|
998
|
+
```
|
|
973
999
|
|
|
974
|
-
|
|
1000
|
+
### Production setup
|
|
975
1001
|
|
|
976
1002
|
When running GoodJob in a production environment, you should be mindful of:
|
|
977
1003
|
|
|
@@ -986,7 +1012,7 @@ The recommended way to monitor the queue in production is:
|
|
|
986
1012
|
- keep an eye on the number of jobs in the queue (abnormal high number of unscheduled jobs means the queue could be underperforming)
|
|
987
1013
|
- consider performance monitoring services which support the built-in Rails instrumentation (eg. Sentry, Skylight, etc.)
|
|
988
1014
|
|
|
989
|
-
|
|
1015
|
+
### Queue performance with Queue Select Limit
|
|
990
1016
|
|
|
991
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.
|
|
992
1018
|
|
|
@@ -4,9 +4,9 @@ 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
|
|
8
9
|
include Filterable
|
|
9
|
-
include Lockable
|
|
10
10
|
include Reportable
|
|
11
11
|
|
|
12
12
|
self.table_name = 'good_jobs'
|
|
@@ -60,5 +60,28 @@ module GoodJob
|
|
|
60
60
|
def discrete?
|
|
61
61
|
self.class.discrete_support? && is_discrete?
|
|
62
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
|
|
63
86
|
end
|
|
64
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)
|
|
@@ -508,19 +508,6 @@ module GoodJob
|
|
|
508
508
|
self.scheduled_at ||= current_time
|
|
509
509
|
end
|
|
510
510
|
|
|
511
|
-
# Build an ActiveJob instance and deserialize the arguments, using `#active_job_data`.
|
|
512
|
-
#
|
|
513
|
-
# @param ignore_deserialization_errors [Boolean]
|
|
514
|
-
# Whether to ignore ActiveJob::DeserializationError when deserializing the arguments.
|
|
515
|
-
# This is most useful if you aren't planning to use the arguments directly.
|
|
516
|
-
def active_job(ignore_deserialization_errors: false)
|
|
517
|
-
ActiveJob::Base.deserialize(active_job_data).tap do |aj|
|
|
518
|
-
aj.send(:deserialize_arguments_if_needed)
|
|
519
|
-
rescue ActiveJob::DeserializationError
|
|
520
|
-
raise unless ignore_deserialization_errors
|
|
521
|
-
end
|
|
522
|
-
end
|
|
523
|
-
|
|
524
511
|
# Return formatted serialized_params for display in the dashboard
|
|
525
512
|
# @return [Hash]
|
|
526
513
|
def display_serialized_params
|
|
@@ -565,14 +552,6 @@ module GoodJob
|
|
|
565
552
|
|
|
566
553
|
private
|
|
567
554
|
|
|
568
|
-
def active_job_data
|
|
569
|
-
serialized_params.deep_dup
|
|
570
|
-
.tap do |job_data|
|
|
571
|
-
job_data["provider_job_id"] = id
|
|
572
|
-
job_data["good_job_concurrency_key"] = concurrency_key if concurrency_key
|
|
573
|
-
end
|
|
574
|
-
end
|
|
575
|
-
|
|
576
555
|
def reset_batch_values(&block)
|
|
577
556
|
GoodJob::Batch.within_thread(batch_id: nil, batch_callback_id: nil, &block)
|
|
578
557
|
end
|
|
@@ -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/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,6 +247,9 @@ module GoodJob
|
|
|
246
247
|
end
|
|
247
248
|
end
|
|
248
249
|
|
|
250
|
+
include ActiveSupport::Deprecation::DeprecatedConstantAccessor
|
|
251
|
+
deprecate_constant :Lockable, 'GoodJob::AdvisoryLockable', deprecator: deprecator
|
|
252
|
+
|
|
249
253
|
# Whether all GoodJob migrations have been applied.
|
|
250
254
|
# For use in tests/CI to validate GoodJob is up-to-date.
|
|
251
255
|
# @return [Boolean]
|
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:
|