solid_queue_guard 1.2.5 → 1.3.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 +20 -0
- data/Gemfile.lock +1 -1
- data/lib/solid_queue_guard/checks/config/process_heartbeat_config_check.rb +98 -17
- data/lib/solid_queue_guard/checks/config/thread_pool_check.rb +5 -5
- data/lib/solid_queue_guard/configuration_sizing.rb +24 -0
- data/lib/solid_queue_guard/recommendations/topology.rb +6 -6
- data/lib/solid_queue_guard/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 437b060eb21ee49c674d1f5ddbb34471209bbecd11eef1a99a562249f134b1f1
|
|
4
|
+
data.tar.gz: 0fb8e3a58e206ee4a0b78a4c815efb7397c1d5d55e153360d7655eee21ff81fa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9fd08d490a2dff9d8e4915ed2742a660433c781a3244c321097715aac51c73729c91b5ad2cbcc90200c53c336c6a5f7cd80feb3eaebd426e025426700b5f1db5
|
|
7
|
+
data.tar.gz: da19ef71cf30e339ac2576a1749064f4ae904491bc0a67fdd61a9593feea6f2a48a051d13f260d6d0f739a43ed5aaddc5b5ef51e2183f98a58b36dbfdb3888bd
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.1] - 2026-07-31
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- `thread_pool` and topology pool recommendations work with Solid Queue 1.6+, which renamed
|
|
13
|
+
`estimated_number_of_threads` to `estimated_database_pool_size`. Guard now reads whichever
|
|
14
|
+
private helper the installed gem exposes.
|
|
15
|
+
|
|
16
|
+
## [1.3.0] - 2026-07-29
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `process_heartbeat_config` no longer warns just because `process_heartbeat_interval` and
|
|
21
|
+
`process_alive_threshold` keep the Solid Queue defaults — the defaults now pass. The check
|
|
22
|
+
validates the relationship between the values instead: it fails when `process_alive_threshold`
|
|
23
|
+
is not above the heartbeat interval, and warns when the margin tolerates less than
|
|
24
|
+
`min_margin` heartbeats (default 2) or when Guard's `stale_process_threshold` is above
|
|
25
|
+
`process_alive_threshold`
|
|
26
|
+
- `config.checks.process_heartbeat_config = { min_margin: 3 }` tunes the required margin
|
|
27
|
+
|
|
8
28
|
## [1.2.5] - 2026-07-10
|
|
9
29
|
|
|
10
30
|
### Fixed
|
data/Gemfile.lock
CHANGED
|
@@ -3,26 +3,107 @@
|
|
|
3
3
|
module SolidQueueGuard
|
|
4
4
|
module Checks
|
|
5
5
|
module Config
|
|
6
|
+
# Solid Queue prunes any process whose heartbeat is older than
|
|
7
|
+
# process_alive_threshold, releasing the executions it had claimed. A live
|
|
8
|
+
# worker that merely delayed a heartbeat is pruned the same way, so the
|
|
9
|
+
# threshold must leave room for more than one missed beat.
|
|
6
10
|
class ProcessHeartbeatConfigCheck < Base
|
|
7
|
-
|
|
8
|
-
DEFAULT_ALIVE_THRESHOLD = 5.minutes
|
|
11
|
+
DEFAULT_MIN_MARGIN = 2
|
|
9
12
|
|
|
10
13
|
def call
|
|
11
|
-
|
|
12
|
-
alive_threshold
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)
|
|
25
|
-
|
|
14
|
+
return invalid_heartbeat_interval unless heartbeat_interval.positive?
|
|
15
|
+
return alive_threshold_too_low if alive_threshold <= heartbeat_interval
|
|
16
|
+
return margin_too_tight if margin < min_margin
|
|
17
|
+
return guard_threshold_above_alive if stale_process_threshold > alive_threshold
|
|
18
|
+
|
|
19
|
+
healthy_thresholds
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def healthy_thresholds
|
|
25
|
+
pass(
|
|
26
|
+
check_id,
|
|
27
|
+
"Heartbeat interval: #{humanize(heartbeat_interval)}, " \
|
|
28
|
+
"alive threshold: #{humanize(alive_threshold)} (#{margin.round(1)}x margin)",
|
|
29
|
+
metadata: base_metadata
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def invalid_heartbeat_interval
|
|
34
|
+
failure(
|
|
35
|
+
check_id,
|
|
36
|
+
'process_heartbeat_interval must be a positive duration',
|
|
37
|
+
suggestion: 'Set SolidQueue.process_heartbeat_interval to a positive value (default: 60 seconds)',
|
|
38
|
+
metadata: base_metadata
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def alive_threshold_too_low
|
|
43
|
+
failure(
|
|
44
|
+
check_id,
|
|
45
|
+
"process_alive_threshold (#{humanize(alive_threshold)}) is not greater than " \
|
|
46
|
+
"process_heartbeat_interval (#{humanize(heartbeat_interval)})",
|
|
47
|
+
suggestion: "Raise process_alive_threshold to at least #{humanize(recommended_threshold)} " \
|
|
48
|
+
'so a single late heartbeat does not prune a live process',
|
|
49
|
+
metadata: base_metadata
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def margin_too_tight
|
|
54
|
+
warn(
|
|
55
|
+
check_id,
|
|
56
|
+
"process_alive_threshold (#{humanize(alive_threshold)}) tolerates less than " \
|
|
57
|
+
"#{min_margin} heartbeats of #{humanize(heartbeat_interval)}",
|
|
58
|
+
suggestion: "Raise process_alive_threshold to at least #{humanize(recommended_threshold)} " \
|
|
59
|
+
'so a single late heartbeat does not release claimed jobs',
|
|
60
|
+
metadata: base_metadata
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def guard_threshold_above_alive
|
|
65
|
+
warn(
|
|
66
|
+
check_id,
|
|
67
|
+
"Guard stale_process_threshold (#{humanize(stale_process_threshold)}) is above " \
|
|
68
|
+
"process_alive_threshold (#{humanize(alive_threshold)})",
|
|
69
|
+
suggestion: "Lower stale_process_threshold to at most #{humanize(alive_threshold)}; " \
|
|
70
|
+
'Solid Queue prunes dead process rows before Guard would report them as stale',
|
|
71
|
+
metadata: base_metadata.merge(stale_process_threshold: stale_process_threshold)
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def heartbeat_interval
|
|
76
|
+
@heartbeat_interval ||= SolidQueue.process_heartbeat_interval.to_i
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def alive_threshold
|
|
80
|
+
@alive_threshold ||= SolidQueue.process_alive_threshold.to_i
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def stale_process_threshold
|
|
84
|
+
@stale_process_threshold ||= guard_config.check_setting(
|
|
85
|
+
:stale_process, :threshold, guard_config.stale_process_threshold
|
|
86
|
+
).to_i
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def min_margin
|
|
90
|
+
@min_margin ||= check_setting(:min_margin, DEFAULT_MIN_MARGIN)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def margin
|
|
94
|
+
@margin ||= alive_threshold / heartbeat_interval.to_f
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def recommended_threshold
|
|
98
|
+
heartbeat_interval * min_margin
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def base_metadata
|
|
102
|
+
{ heartbeat_interval: heartbeat_interval, alive_threshold: alive_threshold }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def humanize(seconds)
|
|
106
|
+
ActiveSupport::Duration.build(seconds).inspect
|
|
26
107
|
end
|
|
27
108
|
end
|
|
28
109
|
end
|
|
@@ -5,7 +5,7 @@ module SolidQueueGuard
|
|
|
5
5
|
module Config
|
|
6
6
|
class ThreadPoolCheck < Base
|
|
7
7
|
def call
|
|
8
|
-
|
|
8
|
+
required_pool = ConfigurationSizing.estimated_database_pool_size(solid_queue_configuration)
|
|
9
9
|
pool_size = SolidQueue::Record.connection_pool&.size
|
|
10
10
|
|
|
11
11
|
return skip('thread_pool', 'Queue database connection pool is not available') if pool_size.nil?
|
|
@@ -17,18 +17,18 @@ module SolidQueueGuard
|
|
|
17
17
|
)
|
|
18
18
|
end.max || 1
|
|
19
19
|
|
|
20
|
-
if pool_size >=
|
|
20
|
+
if pool_size >= required_pool
|
|
21
21
|
pass(
|
|
22
22
|
'thread_pool',
|
|
23
23
|
"Worker threads: #{max_worker_threads}, queue DB pool: #{pool_size}",
|
|
24
|
-
metadata: { threads: max_worker_threads, pool: pool_size, required:
|
|
24
|
+
metadata: { threads: max_worker_threads, pool: pool_size, required: required_pool }
|
|
25
25
|
)
|
|
26
26
|
else
|
|
27
27
|
failure(
|
|
28
28
|
'thread_pool',
|
|
29
29
|
"Worker threads: #{max_worker_threads}, queue DB pool: #{pool_size}",
|
|
30
|
-
suggestion: "Increase queue DB pool to at least #{
|
|
31
|
-
metadata: { threads: max_worker_threads, pool: pool_size, required:
|
|
30
|
+
suggestion: "Increase queue DB pool to at least #{required_pool} or reduce worker threads",
|
|
31
|
+
metadata: { threads: max_worker_threads, pool: pool_size, required: required_pool }
|
|
32
32
|
)
|
|
33
33
|
end
|
|
34
34
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
# Reads Solid Queue's estimated queue-DB pool requirement across gem versions.
|
|
5
|
+
#
|
|
6
|
+
# Solid Queue 1.6 renamed the private helper from +estimated_number_of_threads+
|
|
7
|
+
# to +estimated_database_pool_size+ (fiber workers need a different estimate).
|
|
8
|
+
# Both return the same value for classic thread workers: max capacity + 2.
|
|
9
|
+
module ConfigurationSizing
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def estimated_database_pool_size(configuration = SolidQueue::Configuration.new)
|
|
13
|
+
if configuration.respond_to?(:estimated_database_pool_size, true)
|
|
14
|
+
configuration.send(:estimated_database_pool_size)
|
|
15
|
+
elsif configuration.respond_to?(:estimated_number_of_threads, true)
|
|
16
|
+
configuration.send(:estimated_number_of_threads)
|
|
17
|
+
else
|
|
18
|
+
raise NoMethodError,
|
|
19
|
+
'SolidQueue::Configuration has neither estimated_database_pool_size ' \
|
|
20
|
+
'nor estimated_number_of_threads'
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -33,11 +33,11 @@ module SolidQueueGuard
|
|
|
33
33
|
def pool_recommendations
|
|
34
34
|
return async_pool_recommendation if PumaPluginSupport.async_supervisor_mode?
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
required_pool = ConfigurationSizing.estimated_database_pool_size(configuration)
|
|
37
37
|
pool_size = SolidQueue::Record.connection_pool&.size
|
|
38
|
-
return [] if pool_size.nil? ||
|
|
38
|
+
return [] if pool_size.nil? || required_pool <= pool_size
|
|
39
39
|
|
|
40
|
-
["Increase queue DB pool to at least #{
|
|
40
|
+
["Increase queue DB pool to at least #{required_pool} (currently #{pool_size})"]
|
|
41
41
|
rescue StandardError
|
|
42
42
|
[]
|
|
43
43
|
end
|
|
@@ -62,11 +62,11 @@ module SolidQueueGuard
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def async_pool_recommendation
|
|
65
|
-
|
|
65
|
+
required_pool = ConfigurationSizing.estimated_database_pool_size(configuration)
|
|
66
66
|
pool_size = SolidQueue::Record.connection_pool&.size
|
|
67
|
-
return [] if pool_size.nil? ||
|
|
67
|
+
return [] if pool_size.nil? || required_pool <= pool_size
|
|
68
68
|
|
|
69
|
-
["Increase queue DB pool to at least #{
|
|
69
|
+
["Increase queue DB pool to at least #{required_pool} " \
|
|
70
70
|
"for async supervisor mode (currently #{pool_size})"]
|
|
71
71
|
rescue StandardError
|
|
72
72
|
[]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_queue_guard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rafael Pissardo
|
|
@@ -358,6 +358,7 @@ files:
|
|
|
358
358
|
- lib/solid_queue_guard/checks/runtime/stale_process_check.rb
|
|
359
359
|
- lib/solid_queue_guard/cli.rb
|
|
360
360
|
- lib/solid_queue_guard/configuration.rb
|
|
361
|
+
- lib/solid_queue_guard/configuration_sizing.rb
|
|
361
362
|
- lib/solid_queue_guard/engine.rb
|
|
362
363
|
- lib/solid_queue_guard/formatters/json.rb
|
|
363
364
|
- lib/solid_queue_guard/formatters/terminal.rb
|