good_job 3.21.0 → 3.21.1
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 +24 -0
- data/README.md +2 -2
- data/app/models/good_job/batch_record.rb +2 -0
- data/app/models/good_job/execution.rb +8 -9
- data/app/models/good_job/process.rb +8 -2
- data/lib/good_job/capsule.rb +6 -6
- data/lib/good_job/job_performer/metrics.rb +76 -0
- data/lib/good_job/job_performer.rb +31 -3
- data/lib/good_job/multi_scheduler.rb +38 -1
- data/lib/good_job/scheduler.rb +13 -47
- data/lib/good_job/version.rb +1 -1
- data/lib/good_job.rb +1 -0
- metadata +3 -3
- data/lib/good_job/metrics.rb +0 -57
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4f34be14a5677efd6647698f22bcf5189528ca574f34ee5361647f79a02cbfbd
|
|
4
|
+
data.tar.gz: 575a2d5b98556a810b50a31192b3dde8cfd3ab8e9cf09ef57c6efc245e4dc425
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 235c2d8c5519f6af13b5e98586b0e32f1436cf969ce35d8549cecb4e6e9ad172a01a2914e8da1928b37f87b1c47a9317ec3a66dd92412903e5808f308eb67753
|
|
7
|
+
data.tar.gz: b0c1f3ee48467b924dcde22d0529a8728873babf0202c531c615b3c3e5a64a201f094d77b9414cf532cb90e1d8610ebf07176a3e7688ca6639bf71ad52be2592
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v3.21.1](https://github.com/bensheldon/good_job/tree/v3.21.1) (2023-11-14)
|
|
4
|
+
|
|
5
|
+
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.21.0...v3.21.1)
|
|
6
|
+
|
|
7
|
+
**Fixed bugs:**
|
|
8
|
+
|
|
9
|
+
- Explicitly require `active_job/arguments` in `GoodJob::BatchRecord` [\#1150](https://github.com/bensheldon/good_job/pull/1150) ([hidenba](https://github.com/hidenba))
|
|
10
|
+
- Bug: Polling only activates single thread, should eagerly create additional threads when jobs exist [\#1148](https://github.com/bensheldon/good_job/pull/1148) ([bensheldon](https://github.com/bensheldon))
|
|
11
|
+
|
|
12
|
+
**Closed issues:**
|
|
13
|
+
|
|
14
|
+
- Error when executing ActiveJob::Batch.new in Rails 7.1.x [\#1149](https://github.com/bensheldon/good_job/issues/1149)
|
|
15
|
+
- Show whether or not cron scheduler is enable in dashboard \(UI\) [\#1117](https://github.com/bensheldon/good_job/issues/1117)
|
|
16
|
+
- ActiveRecord::ConnectionNotEstablished For rails multi DB [\#1103](https://github.com/bensheldon/good_job/issues/1103)
|
|
17
|
+
- Rails API dies when using latest good\_job version [\#952](https://github.com/bensheldon/good_job/issues/952)
|
|
18
|
+
- config.good\_job.preserve\_job\_records = false not working with CRON [\#927](https://github.com/bensheldon/good_job/issues/927)
|
|
19
|
+
- Pundit::NotDefinedError \(unable to find policy `Admin::GoodJob::JobPolicy` for \) - version 2.13.0 and above [\#618](https://github.com/bensheldon/good_job/issues/618)
|
|
20
|
+
- Running CLI under foreman doesn't display log output until exit [\#490](https://github.com/bensheldon/good_job/issues/490)
|
|
21
|
+
|
|
22
|
+
**Merged pull requests:**
|
|
23
|
+
|
|
24
|
+
- Always instantiate MultiScheduler; delegate Scheduler Metrics to JobPerformer [\#1147](https://github.com/bensheldon/good_job/pull/1147) ([bensheldon](https://github.com/bensheldon))
|
|
25
|
+
- Clarify on concurrency uniqueness constraints [\#1144](https://github.com/bensheldon/good_job/pull/1144) ([Earlopain](https://github.com/Earlopain))
|
|
26
|
+
|
|
3
27
|
## [v3.21.0](https://github.com/bensheldon/good_job/tree/v3.21.0) (2023-11-06)
|
|
4
28
|
|
|
5
29
|
[Full Changelog](https://github.com/bensheldon/good_job/compare/v3.20.0...v3.21.0)
|
data/README.md
CHANGED
|
@@ -470,7 +470,7 @@ class MyJob < ApplicationJob
|
|
|
470
470
|
# Can be String or Lambda/Proc that is invoked in the context of the job.
|
|
471
471
|
# Note: Arguments passed to #perform_later can be accessed through ActiveJob's `arguments` method
|
|
472
472
|
# which is an array containing positional arguments and, optionally, a kwarg hash.
|
|
473
|
-
key: -> { "
|
|
473
|
+
key: -> { "MyJob-#{arguments.first}-#{arguments.last[:version]}" } # MyJob.perform_later("Alice", version: 'v2') => "MyJob-Alice-v2"
|
|
474
474
|
)
|
|
475
475
|
|
|
476
476
|
def perform(first_name)
|
|
@@ -483,7 +483,7 @@ When testing, the resulting concurrency key value can be inspected:
|
|
|
483
483
|
|
|
484
484
|
```ruby
|
|
485
485
|
job = MyJob.perform_later("Alice")
|
|
486
|
-
job.good_job_concurrency_key #=> "
|
|
486
|
+
job.good_job_concurrency_key #=> "MyJob-Alice"
|
|
487
487
|
```
|
|
488
488
|
|
|
489
489
|
#### How concurrency controls work
|
|
@@ -248,6 +248,7 @@ module GoodJob
|
|
|
248
248
|
|
|
249
249
|
# Finds the next eligible Execution, acquire an advisory lock related to it, and
|
|
250
250
|
# executes the job.
|
|
251
|
+
# @yield [Execution, nil] The next eligible Execution, or +nil+ if none found, before it is performed.
|
|
251
252
|
# @return [ExecutionResult, nil]
|
|
252
253
|
# If a job was executed, returns an array with the {Execution} record, the
|
|
253
254
|
# return value for the job's +#perform+ method, and the exception the job
|
|
@@ -256,21 +257,19 @@ module GoodJob
|
|
|
256
257
|
def self.perform_with_advisory_lock(parsed_queues: nil, queue_select_limit: nil)
|
|
257
258
|
execution = nil
|
|
258
259
|
result = nil
|
|
260
|
+
|
|
259
261
|
unfinished.dequeueing_ordered(parsed_queues).only_scheduled.limit(1).with_advisory_lock(select_limit: queue_select_limit) do |executions|
|
|
260
262
|
execution = executions.first
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
if execution&.executable?
|
|
264
|
+
yield(execution) if block_given?
|
|
265
|
+
result = execution.perform
|
|
266
|
+
else
|
|
265
267
|
execution = nil
|
|
266
|
-
|
|
268
|
+
yield(nil) if block_given?
|
|
267
269
|
end
|
|
268
|
-
|
|
269
|
-
yield(execution) if block_given?
|
|
270
|
-
result = execution.perform
|
|
271
270
|
end
|
|
272
|
-
execution&.run_callbacks(:perform_unlocked)
|
|
273
271
|
|
|
272
|
+
execution&.run_callbacks(:perform_unlocked)
|
|
274
273
|
result
|
|
275
274
|
end
|
|
276
275
|
|
|
@@ -52,6 +52,10 @@ module GoodJob # :nodoc:
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def self.ns_current_state
|
|
55
|
+
total_succeeded_executions_count = GoodJob::Scheduler.instances.sum { |scheduler| scheduler.stats.fetch(:succeeded_executions_count, 0) }
|
|
56
|
+
total_errored_executions_count = GoodJob::Scheduler.instances.sum { |scheduler| scheduler.stats.fetch(:errored_executions_count, 0) }
|
|
57
|
+
total_empty_executions_count = GoodJob::Scheduler.instances.sum { |scheduler| scheduler.stats.fetch(:empty_executions_count, 0) }
|
|
58
|
+
|
|
55
59
|
{
|
|
56
60
|
id: ns_current_id,
|
|
57
61
|
hostname: Socket.gethostname,
|
|
@@ -61,8 +65,10 @@ module GoodJob # :nodoc:
|
|
|
61
65
|
retry_on_unhandled_error: GoodJob.retry_on_unhandled_error,
|
|
62
66
|
schedulers: GoodJob::Scheduler.instances.map(&:stats),
|
|
63
67
|
cron_enabled: GoodJob.configuration.enable_cron?,
|
|
64
|
-
total_succeeded_executions_count:
|
|
65
|
-
total_errored_executions_count:
|
|
68
|
+
total_succeeded_executions_count: total_succeeded_executions_count,
|
|
69
|
+
total_errored_executions_count: total_errored_executions_count,
|
|
70
|
+
total_executions_count: total_succeeded_executions_count + total_errored_executions_count,
|
|
71
|
+
total_empty_executions_count: total_empty_executions_count,
|
|
66
72
|
database_connection_pool: {
|
|
67
73
|
size: connection_pool.size,
|
|
68
74
|
active: connection_pool.connections.count(&:in_use?),
|
data/lib/good_job/capsule.rb
CHANGED
|
@@ -32,9 +32,9 @@ module GoodJob
|
|
|
32
32
|
@shared_executor = GoodJob::SharedExecutor.new
|
|
33
33
|
@notifier = GoodJob::Notifier.new(enable_listening: @configuration.enable_listen_notify, executor: @shared_executor.executor)
|
|
34
34
|
@poller = GoodJob::Poller.new(poll_interval: @configuration.poll_interval)
|
|
35
|
-
@
|
|
36
|
-
@notifier.recipients
|
|
37
|
-
@poller.recipients
|
|
35
|
+
@multi_scheduler = GoodJob::MultiScheduler.from_configuration(@configuration, warm_cache_on_initialize: true)
|
|
36
|
+
@notifier.recipients.push([@multi_scheduler, :create_thread])
|
|
37
|
+
@poller.recipients.push(-> { @multi_scheduler.create_thread({ fanout: true }) })
|
|
38
38
|
|
|
39
39
|
@cron_manager = GoodJob::CronManager.new(@configuration.cron_entries, start_on_initialize: true, executor: @shared_executor.executor) if @configuration.enable_cron?
|
|
40
40
|
|
|
@@ -52,7 +52,7 @@ module GoodJob
|
|
|
52
52
|
# @return [void]
|
|
53
53
|
def shutdown(timeout: NONE)
|
|
54
54
|
timeout = @configuration.shutdown_timeout if timeout == NONE
|
|
55
|
-
GoodJob._shutdown_all([@shared_executor, @notifier, @poller, @
|
|
55
|
+
GoodJob._shutdown_all([@shared_executor, @notifier, @poller, @multi_scheduler, @cron_manager].compact, timeout: timeout)
|
|
56
56
|
@startable = false
|
|
57
57
|
@running = false
|
|
58
58
|
end
|
|
@@ -74,7 +74,7 @@ module GoodJob
|
|
|
74
74
|
|
|
75
75
|
# @return [Boolean] Whether the capsule has been shutdown.
|
|
76
76
|
def shutdown?
|
|
77
|
-
[@shared_executor, @notifier, @poller, @
|
|
77
|
+
[@shared_executor, @notifier, @poller, @multi_scheduler, @cron_manager].compact.all?(&:shutdown?)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
# Creates an execution thread(s) with the given attributes.
|
|
@@ -82,7 +82,7 @@ module GoodJob
|
|
|
82
82
|
# @return [Boolean, nil] Whether the thread was created.
|
|
83
83
|
def create_thread(job_state = nil)
|
|
84
84
|
start if startable?
|
|
85
|
-
@
|
|
85
|
+
@multi_scheduler&.create_thread(job_state)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
private
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent/atomic/atomic_fixnum'
|
|
4
|
+
|
|
5
|
+
module GoodJob # :nodoc:
|
|
6
|
+
class JobPerformer
|
|
7
|
+
# Metrics for the scheduler.
|
|
8
|
+
class Metrics
|
|
9
|
+
def initialize
|
|
10
|
+
@mutex = Mutex.new
|
|
11
|
+
@empty_executions = Concurrent::AtomicFixnum.new
|
|
12
|
+
@errored_executions = Concurrent::AtomicFixnum.new
|
|
13
|
+
@succeeded_executions = Concurrent::AtomicFixnum.new
|
|
14
|
+
@execution_at = nil
|
|
15
|
+
@check_queue_at = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Increments number of failed executions.
|
|
19
|
+
# @return [Integer]
|
|
20
|
+
def increment_errored_executions
|
|
21
|
+
@execution_at = Time.current
|
|
22
|
+
@errored_executions.increment
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Increments number of succeeded executions.
|
|
26
|
+
# @return [Integer]
|
|
27
|
+
def increment_succeeded_executions
|
|
28
|
+
@execution_at = Time.current
|
|
29
|
+
@succeeded_executions.increment
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Increments number of dequeue attempts with no executions.
|
|
33
|
+
# @return [Integer]
|
|
34
|
+
def increment_empty_executions
|
|
35
|
+
@execution_at = Time.current
|
|
36
|
+
@empty_executions.increment
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Last time a job was executed (started or finished).
|
|
40
|
+
# @return [Time, nil]
|
|
41
|
+
def touch_execution_at
|
|
42
|
+
@execution_at = Time.current
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Last time the queue was checked for jobs.
|
|
46
|
+
# @return [Time, nil]
|
|
47
|
+
def touch_check_queue_at
|
|
48
|
+
@check_queue_at = Time.current
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# All metrics in a Hash.
|
|
52
|
+
# @return [Hash]
|
|
53
|
+
def to_h
|
|
54
|
+
{
|
|
55
|
+
empty_executions_count: @empty_executions.value,
|
|
56
|
+
errored_executions_count: @errored_executions.value,
|
|
57
|
+
succeeded_executions_count: @succeeded_executions.value,
|
|
58
|
+
}.tap do |values|
|
|
59
|
+
values[:total_executions_count] = values[:succeeded_executions_count] + values[:errored_executions_count]
|
|
60
|
+
values[:execution_at] = @execution_at
|
|
61
|
+
values[:check_queue_at] = @check_queue_at
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Reset counters.
|
|
66
|
+
# @return [void]
|
|
67
|
+
def reset
|
|
68
|
+
@empty_executions.value = 0
|
|
69
|
+
@errored_executions.value = 0
|
|
70
|
+
@succeeded_executions.value = 0
|
|
71
|
+
@execution_at = nil
|
|
72
|
+
@check_queue_at = nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -16,6 +16,7 @@ module GoodJob
|
|
|
16
16
|
# @param queue_string [String] Queues to execute jobs from
|
|
17
17
|
def initialize(queue_string)
|
|
18
18
|
@queue_string = queue_string
|
|
19
|
+
@metrics = Metrics.new
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
# A meaningful name to identify the performer in logs and for debugging.
|
|
@@ -25,15 +26,28 @@ module GoodJob
|
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
# Perform the next eligible job
|
|
29
|
+
# @yield [Execution] Yields the execution, if one is dequeued
|
|
28
30
|
# @return [Object, nil] Returns job result or +nil+ if no job was found
|
|
29
31
|
def next
|
|
30
32
|
active_job_id = nil
|
|
31
33
|
job_query.perform_with_advisory_lock(parsed_queues: parsed_queues, queue_select_limit: GoodJob.configuration.queue_select_limit) do |execution|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
@metrics.touch_check_queue_at
|
|
35
|
+
|
|
36
|
+
if execution
|
|
37
|
+
active_job_id = execution.active_job_id
|
|
38
|
+
performing_active_job_ids << active_job_id
|
|
39
|
+
@metrics.touch_execution_at
|
|
40
|
+
yield(execution) if block_given?
|
|
41
|
+
else
|
|
42
|
+
@metrics.increment_empty_executions
|
|
43
|
+
end
|
|
44
|
+
end.tap do |result|
|
|
45
|
+
if result
|
|
46
|
+
result.succeeded? ? @metrics.increment_succeeded_executions : @metrics.increment_errored_executions
|
|
47
|
+
end
|
|
34
48
|
end
|
|
35
49
|
ensure
|
|
36
|
-
performing_active_job_ids.delete(active_job_id)
|
|
50
|
+
performing_active_job_ids.delete(active_job_id) if active_job_id
|
|
37
51
|
end
|
|
38
52
|
|
|
39
53
|
# Tests whether this performer should be used in GoodJob's current state.
|
|
@@ -72,6 +86,20 @@ module GoodJob
|
|
|
72
86
|
GoodJob.cleanup_preserved_jobs
|
|
73
87
|
end
|
|
74
88
|
|
|
89
|
+
# Metrics about this performer
|
|
90
|
+
# @return [Hash]
|
|
91
|
+
def stats
|
|
92
|
+
{
|
|
93
|
+
name: name,
|
|
94
|
+
}.merge(@metrics.to_h)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Reset metrics about this performer
|
|
98
|
+
# @return [void]
|
|
99
|
+
def reset_stats
|
|
100
|
+
@metrics.reset
|
|
101
|
+
end
|
|
102
|
+
|
|
75
103
|
private
|
|
76
104
|
|
|
77
105
|
attr_reader :queue_string
|
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
module GoodJob
|
|
4
4
|
# Delegates the interface of a single {Scheduler} to multiple Schedulers.
|
|
5
5
|
class MultiScheduler
|
|
6
|
+
# Creates MultiScheduler from a GoodJob::Configuration instance.
|
|
7
|
+
# @param configuration [GoodJob::Configuration]
|
|
8
|
+
# @param warm_cache_on_initialize [Boolean]
|
|
9
|
+
# @return [GoodJob::MultiScheduler]
|
|
10
|
+
def self.from_configuration(configuration, warm_cache_on_initialize: false)
|
|
11
|
+
schedulers = configuration.queue_string.split(';').map do |queue_string_and_max_threads|
|
|
12
|
+
queue_string, max_threads = queue_string_and_max_threads.split(':')
|
|
13
|
+
max_threads = (max_threads || configuration.max_threads).to_i
|
|
14
|
+
|
|
15
|
+
job_performer = GoodJob::JobPerformer.new(queue_string)
|
|
16
|
+
GoodJob::Scheduler.new(
|
|
17
|
+
job_performer,
|
|
18
|
+
max_threads: max_threads,
|
|
19
|
+
max_cache: configuration.max_cache,
|
|
20
|
+
warm_cache_on_initialize: warm_cache_on_initialize,
|
|
21
|
+
cleanup_interval_seconds: configuration.cleanup_interval_seconds,
|
|
22
|
+
cleanup_interval_jobs: configuration.cleanup_interval_jobs
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
new(schedulers)
|
|
27
|
+
end
|
|
28
|
+
|
|
6
29
|
# @return [Array<Scheduler>] List of the scheduler delegates
|
|
7
30
|
attr_reader :schedulers
|
|
8
31
|
|
|
@@ -43,7 +66,7 @@ module GoodJob
|
|
|
43
66
|
def create_thread(state = nil)
|
|
44
67
|
results = []
|
|
45
68
|
|
|
46
|
-
if state
|
|
69
|
+
if state && !state[:fanout]
|
|
47
70
|
schedulers.any? do |scheduler|
|
|
48
71
|
scheduler.create_thread(state).tap { |result| results << result }
|
|
49
72
|
end
|
|
@@ -61,5 +84,19 @@ module GoodJob
|
|
|
61
84
|
nil
|
|
62
85
|
end
|
|
63
86
|
end
|
|
87
|
+
|
|
88
|
+
def stats
|
|
89
|
+
scheduler_stats = schedulers.map(&:stats)
|
|
90
|
+
|
|
91
|
+
{
|
|
92
|
+
schedulers: scheduler_stats,
|
|
93
|
+
empty_executions_count: scheduler_stats.sum { |stats| stats.fetch(:empty_executions_count, 0) },
|
|
94
|
+
errored_executions_count: scheduler_stats.sum { |stats| stats.fetch(:errored_executions_count, 0) },
|
|
95
|
+
succeeded_executions_count: scheduler_stats.sum { |stats| stats.fetch(:succeeded_executions_count, 0) },
|
|
96
|
+
total_executions_count: scheduler_stats.sum { |stats| stats.fetch(:total_executions_count, 0) },
|
|
97
|
+
execution_at: scheduler_stats.map { |stats| stats.fetch(:execution_at, nil) }.compact.max,
|
|
98
|
+
check_queue_at: scheduler_stats.map { |stats| stats.fetch(:check_queue_at, nil) }.compact.max,
|
|
99
|
+
}
|
|
100
|
+
end
|
|
64
101
|
end
|
|
65
102
|
end
|
data/lib/good_job/scheduler.rb
CHANGED
|
@@ -4,7 +4,6 @@ require "concurrent/executor/thread_pool_executor"
|
|
|
4
4
|
require "concurrent/executor/timer_set"
|
|
5
5
|
require "concurrent/scheduled_task"
|
|
6
6
|
require "concurrent/utility/processor_counter"
|
|
7
|
-
require 'good_job/metrics'
|
|
8
7
|
|
|
9
8
|
module GoodJob # :nodoc:
|
|
10
9
|
#
|
|
@@ -36,33 +35,6 @@ module GoodJob # :nodoc:
|
|
|
36
35
|
# @return [Array<GoodJob::Scheduler>, nil]
|
|
37
36
|
cattr_reader :instances, default: Concurrent::Array.new, instance_reader: false
|
|
38
37
|
|
|
39
|
-
# Creates GoodJob::Scheduler(s) and Performers from a GoodJob::Configuration instance.
|
|
40
|
-
# @param configuration [GoodJob::Configuration]
|
|
41
|
-
# @param warm_cache_on_initialize [Boolean]
|
|
42
|
-
# @return [GoodJob::Scheduler, GoodJob::MultiScheduler]
|
|
43
|
-
def self.from_configuration(configuration, warm_cache_on_initialize: false)
|
|
44
|
-
schedulers = configuration.queue_string.split(';').map do |queue_string_and_max_threads|
|
|
45
|
-
queue_string, max_threads = queue_string_and_max_threads.split(':')
|
|
46
|
-
max_threads = (max_threads || configuration.max_threads).to_i
|
|
47
|
-
|
|
48
|
-
job_performer = GoodJob::JobPerformer.new(queue_string)
|
|
49
|
-
GoodJob::Scheduler.new(
|
|
50
|
-
job_performer,
|
|
51
|
-
max_threads: max_threads,
|
|
52
|
-
max_cache: configuration.max_cache,
|
|
53
|
-
warm_cache_on_initialize: warm_cache_on_initialize,
|
|
54
|
-
cleanup_interval_seconds: configuration.cleanup_interval_seconds,
|
|
55
|
-
cleanup_interval_jobs: configuration.cleanup_interval_jobs
|
|
56
|
-
)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
if schedulers.size > 1
|
|
60
|
-
GoodJob::MultiScheduler.new(schedulers)
|
|
61
|
-
else
|
|
62
|
-
schedulers.first
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
38
|
# Human readable name of the scheduler that includes configuration values.
|
|
67
39
|
# @return [String]
|
|
68
40
|
attr_reader :name
|
|
@@ -88,7 +60,6 @@ module GoodJob # :nodoc:
|
|
|
88
60
|
@executor_options[:name] = name
|
|
89
61
|
|
|
90
62
|
@cleanup_tracker = CleanupTracker.new(cleanup_interval_seconds: cleanup_interval_seconds, cleanup_interval_jobs: cleanup_interval_jobs)
|
|
91
|
-
@metrics = ::GoodJob::Metrics.new
|
|
92
63
|
@executor_options[:name] = name
|
|
93
64
|
|
|
94
65
|
create_executor
|
|
@@ -143,7 +114,7 @@ module GoodJob # :nodoc:
|
|
|
143
114
|
|
|
144
115
|
instrument("scheduler_restart_pools") do
|
|
145
116
|
shutdown(timeout: timeout)
|
|
146
|
-
@
|
|
117
|
+
@performer.reset_stats
|
|
147
118
|
create_executor
|
|
148
119
|
warm_cache
|
|
149
120
|
end
|
|
@@ -152,16 +123,17 @@ module GoodJob # :nodoc:
|
|
|
152
123
|
# Wakes a thread to allow the performer to execute a task.
|
|
153
124
|
# @param state [Hash, nil] Contextual information for the performer. See {JobPerformer#next?}.
|
|
154
125
|
# @return [Boolean, nil] Whether work was started.
|
|
155
|
-
#
|
|
156
126
|
# * +nil+ if the scheduler is unable to take new work, for example if the thread pool is shut down or at capacity.
|
|
157
127
|
# * +true+ if the performer started executing work.
|
|
158
128
|
# * +false+ if the performer decides not to attempt to execute a task based on the +state+ that is passed to it.
|
|
159
129
|
def create_thread(state = nil)
|
|
160
130
|
return nil unless executor.running?
|
|
161
131
|
|
|
162
|
-
if state
|
|
132
|
+
if state.present?
|
|
163
133
|
return false unless performer.next?(state)
|
|
164
134
|
|
|
135
|
+
fanout = state&.fetch(:fanout, nil)
|
|
136
|
+
|
|
165
137
|
if state[:count]
|
|
166
138
|
# When given state for multiple jobs, try to create a thread for each one.
|
|
167
139
|
# Return true if a thread can be created for all of them, nil if partial or none.
|
|
@@ -193,7 +165,7 @@ module GoodJob # :nodoc:
|
|
|
193
165
|
return nil unless remaining_cache_count.positive?
|
|
194
166
|
end
|
|
195
167
|
|
|
196
|
-
create_task(delay)
|
|
168
|
+
create_task(delay, fanout: fanout)
|
|
197
169
|
|
|
198
170
|
run_now ? true : nil
|
|
199
171
|
end
|
|
@@ -207,16 +179,6 @@ module GoodJob # :nodoc:
|
|
|
207
179
|
unhandled_error = thread_error || result&.unhandled_error
|
|
208
180
|
GoodJob._on_thread_error(unhandled_error) if unhandled_error
|
|
209
181
|
|
|
210
|
-
if unhandled_error || result&.handled_error
|
|
211
|
-
@metrics.increment_errored_executions
|
|
212
|
-
elsif result&.unexecutable
|
|
213
|
-
@metrics.increment_unexecutable_executions
|
|
214
|
-
elsif result
|
|
215
|
-
@metrics.increment_succeeded_executions
|
|
216
|
-
else
|
|
217
|
-
@metrics.increment_empty_executions
|
|
218
|
-
end
|
|
219
|
-
|
|
220
182
|
instrument("finished_job_task", { result: output, error: thread_error, time: time })
|
|
221
183
|
return unless output
|
|
222
184
|
|
|
@@ -232,6 +194,7 @@ module GoodJob # :nodoc:
|
|
|
232
194
|
# @return [Hash]
|
|
233
195
|
def stats
|
|
234
196
|
available_threads = executor.ready_worker_count
|
|
197
|
+
|
|
235
198
|
{
|
|
236
199
|
name: name,
|
|
237
200
|
queues: performer.name,
|
|
@@ -241,7 +204,7 @@ module GoodJob # :nodoc:
|
|
|
241
204
|
max_cache: @max_cache,
|
|
242
205
|
active_cache: cache_count,
|
|
243
206
|
available_cache: remaining_cache_count,
|
|
244
|
-
}.merge!(@
|
|
207
|
+
}.merge!(@performer.stats.without(:name))
|
|
245
208
|
end
|
|
246
209
|
|
|
247
210
|
# Preload existing runnable and future-scheduled jobs
|
|
@@ -300,12 +263,15 @@ module GoodJob # :nodoc:
|
|
|
300
263
|
end
|
|
301
264
|
|
|
302
265
|
# @param delay [Integer]
|
|
266
|
+
# @param fanout [Boolean] Whether to eagerly create a 2nd execution thread if a job is found.
|
|
303
267
|
# @return [void]
|
|
304
|
-
def create_task(delay = 0)
|
|
305
|
-
future = Concurrent::ScheduledTask.new(delay, args: [performer], executor: executor, timer_set: timer_set) do |thr_performer|
|
|
268
|
+
def create_task(delay = 0, fanout: false)
|
|
269
|
+
future = Concurrent::ScheduledTask.new(delay, args: [self, performer], executor: executor, timer_set: timer_set) do |thr_scheduler, thr_performer|
|
|
306
270
|
Thread.current.name = Thread.current.name.sub("-worker-", "-thread-") if Thread.current.name
|
|
307
271
|
Rails.application.reloader.wrap do
|
|
308
|
-
thr_performer.next
|
|
272
|
+
thr_performer.next do |found|
|
|
273
|
+
thr_scheduler.create_thread({ fanout: fanout }) if found && fanout
|
|
274
|
+
end
|
|
309
275
|
end
|
|
310
276
|
end
|
|
311
277
|
future.add_observer(self, :task_observer)
|
data/lib/good_job/version.rb
CHANGED
data/lib/good_job.rb
CHANGED
|
@@ -26,6 +26,7 @@ require 'good_job/current_thread'
|
|
|
26
26
|
require "good_job/daemon"
|
|
27
27
|
require "good_job/dependencies"
|
|
28
28
|
require "good_job/job_performer"
|
|
29
|
+
require "good_job/job_performer/metrics"
|
|
29
30
|
require "good_job/log_subscriber"
|
|
30
31
|
require "good_job/multi_scheduler"
|
|
31
32
|
require "good_job/notifier"
|
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.21.
|
|
4
|
+
version: 3.21.1
|
|
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-11-
|
|
11
|
+
date: 2023-11-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activejob
|
|
@@ -366,8 +366,8 @@ files:
|
|
|
366
366
|
- lib/good_job/http_server.rb
|
|
367
367
|
- lib/good_job/interrupt_error.rb
|
|
368
368
|
- lib/good_job/job_performer.rb
|
|
369
|
+
- lib/good_job/job_performer/metrics.rb
|
|
369
370
|
- lib/good_job/log_subscriber.rb
|
|
370
|
-
- lib/good_job/metrics.rb
|
|
371
371
|
- lib/good_job/multi_scheduler.rb
|
|
372
372
|
- lib/good_job/notifier.rb
|
|
373
373
|
- lib/good_job/notifier/process_heartbeat.rb
|
data/lib/good_job/metrics.rb
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module GoodJob # :nodoc:
|
|
4
|
-
# Metrics for the scheduler.
|
|
5
|
-
class Metrics
|
|
6
|
-
def initialize
|
|
7
|
-
@empty_executions = Concurrent::AtomicFixnum.new
|
|
8
|
-
@errored_executions = Concurrent::AtomicFixnum.new
|
|
9
|
-
@succeeded_executions = Concurrent::AtomicFixnum.new
|
|
10
|
-
@unexecutable_executions = Concurrent::AtomicFixnum.new
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Increments number of empty queried executions.
|
|
14
|
-
# @return [Integer]
|
|
15
|
-
def increment_empty_executions
|
|
16
|
-
@empty_executions.increment
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Increments number of failed executions.
|
|
20
|
-
# @return [Integer]
|
|
21
|
-
def increment_errored_executions
|
|
22
|
-
@errored_executions.increment
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Increments number of succeeded executions.
|
|
26
|
-
# @return [Integer]
|
|
27
|
-
def increment_succeeded_executions
|
|
28
|
-
@succeeded_executions.increment
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Increments number of unlocked executions.
|
|
32
|
-
# @return [Integer]
|
|
33
|
-
def increment_unexecutable_executions
|
|
34
|
-
@unexecutable_executions.increment
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def to_h
|
|
38
|
-
{
|
|
39
|
-
empty_executions_count: @empty_executions.value,
|
|
40
|
-
errored_executions_count: @errored_executions.value,
|
|
41
|
-
succeeded_executions_count: @succeeded_executions.value,
|
|
42
|
-
unexecutable_executions_count: @unexecutable_executions.value,
|
|
43
|
-
}.tap do |values|
|
|
44
|
-
values[:total_executions_count] = values.values.sum
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Reset counters.
|
|
49
|
-
# @return [void]
|
|
50
|
-
def reset
|
|
51
|
-
@empty_executions.value = 0
|
|
52
|
-
@errored_executions.value = 0
|
|
53
|
-
@succeeded_executions.value = 0
|
|
54
|
-
@unexecutable_executions.value = 0
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|