solid_queue_autoscaler 1.0.11 → 1.0.13
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 +8 -0
- data/lib/solid_queue_autoscaler/adapters/base.rb +30 -0
- data/lib/solid_queue_autoscaler/adapters/heroku.rb +2 -24
- data/lib/solid_queue_autoscaler/adapters/kubernetes.rb +14 -25
- data/lib/solid_queue_autoscaler/autoscale_job.rb +10 -8
- data/lib/solid_queue_autoscaler/railtie.rb +5 -0
- data/lib/solid_queue_autoscaler/version.rb +1 -1
- data/lib/solid_queue_autoscaler.rb +27 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf4f38fa3806f153c03715b02554d1a82837da595fb9dadf4fb8063d6f52b3c8
|
|
4
|
+
data.tar.gz: 3737a7de81ab147dbd8e38fc87a2f601c21ea4d895758418f4bd484b0483ea12
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ef6933dfe8a7936ba524ebd2fc94f46b20b6545b2e008e6d5e2c5c4753f54f866b82ef84eb75b39aa9d49e69ca476bae9be36cfdfff9cd39f97a380627e38f0
|
|
7
|
+
data.tar.gz: 14b04452165bac891d292dfdcb4dd7fb11993d6b5c772c43e658e2492437bcf7af2b9004f616b1caf9b73e606bb018855cd9f36be7b7a0909f336be59fc34952
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.12] - 2025-01-17
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Fixed AutoscaleJob being enqueued to "default" queue** - Added `queue_as :autoscaler` to the job class
|
|
14
|
+
- The issue was that SolidQueue recurring jobs capture the queue name during initialization, BEFORE Rails `after_initialize` hooks run
|
|
15
|
+
- Without a static `queue_as` in the class, jobs defaulted to the "default" queue
|
|
16
|
+
- The `apply_job_settings!` method can still override this via configuration, but the default must be set in the class for SolidQueue recurring to work correctly
|
|
17
|
+
|
|
10
18
|
## [1.0.11] - 2025-01-17
|
|
11
19
|
|
|
12
20
|
### Fixed
|
|
@@ -32,6 +32,10 @@ module SolidQueueAutoscaler
|
|
|
32
32
|
# end
|
|
33
33
|
# end
|
|
34
34
|
class Base
|
|
35
|
+
# Default retry configuration for transient network errors
|
|
36
|
+
DEFAULT_MAX_RETRIES = 3
|
|
37
|
+
DEFAULT_RETRY_DELAYS = [1, 2, 4].freeze # Exponential backoff in seconds
|
|
38
|
+
|
|
35
39
|
# @param config [Configuration] the autoscaler configuration
|
|
36
40
|
def initialize(config:)
|
|
37
41
|
@config = config
|
|
@@ -97,6 +101,32 @@ module SolidQueueAutoscaler
|
|
|
97
101
|
def log_dry_run(message)
|
|
98
102
|
logger.info("[DRY RUN] #{message}")
|
|
99
103
|
end
|
|
104
|
+
|
|
105
|
+
# Executes a block with retry logic for transient errors.
|
|
106
|
+
# Uses exponential backoff with configurable delays.
|
|
107
|
+
#
|
|
108
|
+
# @param error_classes [Array<Class>] Exception classes that should trigger a retry
|
|
109
|
+
# @param max_retries [Integer] Maximum number of retry attempts (default: 3)
|
|
110
|
+
# @param delays [Array<Integer>] Delay in seconds for each retry (default: [1, 2, 4])
|
|
111
|
+
# @param retryable_check [Proc, nil] Optional proc to determine if a specific error should be retried
|
|
112
|
+
# @yield The block to execute with retry logic
|
|
113
|
+
# @return [Object] The result of the block
|
|
114
|
+
def with_retry(error_classes, max_retries: DEFAULT_MAX_RETRIES, delays: DEFAULT_RETRY_DELAYS, retryable_check: nil)
|
|
115
|
+
attempts = 0
|
|
116
|
+
begin
|
|
117
|
+
attempts += 1
|
|
118
|
+
yield
|
|
119
|
+
rescue *error_classes => e
|
|
120
|
+
should_retry = retryable_check ? retryable_check.call(e) : true
|
|
121
|
+
if attempts < max_retries && should_retry
|
|
122
|
+
delay = delays[attempts - 1] || delays.last
|
|
123
|
+
logger&.warn("[Autoscaler] #{name} API error (attempt #{attempts}/#{max_retries}), retrying in #{delay}s: #{e.message}")
|
|
124
|
+
sleep(delay)
|
|
125
|
+
retry
|
|
126
|
+
end
|
|
127
|
+
raise
|
|
128
|
+
end
|
|
129
|
+
end
|
|
100
130
|
end
|
|
101
131
|
end
|
|
102
132
|
end
|
|
@@ -20,10 +20,6 @@ module SolidQueueAutoscaler
|
|
|
20
20
|
# config.process_type = 'worker'
|
|
21
21
|
# end
|
|
22
22
|
class Heroku < Base
|
|
23
|
-
# Retry configuration for transient network errors
|
|
24
|
-
MAX_RETRIES = 3
|
|
25
|
-
RETRY_DELAYS = [1, 2, 4].freeze # Exponential backoff in seconds
|
|
26
|
-
|
|
27
23
|
# Errors that are safe to retry (transient network issues)
|
|
28
24
|
RETRYABLE_ERRORS = [
|
|
29
25
|
Excon::Error::Timeout,
|
|
@@ -32,7 +28,7 @@ module SolidQueueAutoscaler
|
|
|
32
28
|
].freeze
|
|
33
29
|
|
|
34
30
|
def current_workers
|
|
35
|
-
with_retry do
|
|
31
|
+
with_retry(RETRYABLE_ERRORS, retryable_check: method(:retryable_error?)) do
|
|
36
32
|
formation = client.formation.info(app_name, process_type)
|
|
37
33
|
formation['quantity']
|
|
38
34
|
end
|
|
@@ -50,7 +46,7 @@ module SolidQueueAutoscaler
|
|
|
50
46
|
return quantity
|
|
51
47
|
end
|
|
52
48
|
|
|
53
|
-
with_retry do
|
|
49
|
+
with_retry(RETRYABLE_ERRORS, retryable_check: method(:retryable_error?)) do
|
|
54
50
|
client.formation.update(app_name, process_type, { quantity: quantity })
|
|
55
51
|
end
|
|
56
52
|
quantity
|
|
@@ -88,24 +84,6 @@ module SolidQueueAutoscaler
|
|
|
88
84
|
|
|
89
85
|
private
|
|
90
86
|
|
|
91
|
-
# Executes a block with retry logic for transient network errors.
|
|
92
|
-
# Uses exponential backoff: 1s, 2s, 4s delays between retries.
|
|
93
|
-
def with_retry
|
|
94
|
-
attempts = 0
|
|
95
|
-
begin
|
|
96
|
-
attempts += 1
|
|
97
|
-
yield
|
|
98
|
-
rescue *RETRYABLE_ERRORS => e
|
|
99
|
-
if attempts < MAX_RETRIES && retryable_error?(e)
|
|
100
|
-
delay = RETRY_DELAYS[attempts - 1] || RETRY_DELAYS.last
|
|
101
|
-
logger&.warn("[Autoscaler] Heroku API error (attempt #{attempts}/#{MAX_RETRIES}), retrying in #{delay}s: #{e.message}")
|
|
102
|
-
sleep(delay)
|
|
103
|
-
retry
|
|
104
|
-
end
|
|
105
|
-
raise
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
87
|
# Determines if an error should be retried.
|
|
110
88
|
# Retries timeouts and 5xx errors, but not 4xx client errors.
|
|
111
89
|
def retryable_error?(error)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'net/http'
|
|
4
|
+
|
|
3
5
|
module SolidQueueAutoscaler
|
|
4
6
|
module Adapters
|
|
5
7
|
# Kubernetes adapter for scaling Deployment replicas.
|
|
@@ -30,15 +32,21 @@ module SolidQueueAutoscaler
|
|
|
30
32
|
# Kubernetes API path for apps/v1 group
|
|
31
33
|
APPS_API_VERSION = 'apis/apps/v1'
|
|
32
34
|
|
|
33
|
-
# Retry configuration for transient network errors
|
|
34
|
-
MAX_RETRIES = 3
|
|
35
|
-
RETRY_DELAYS = [1, 2, 4].freeze # Exponential backoff in seconds
|
|
36
|
-
|
|
37
35
|
# Default timeout for Kubernetes API calls (seconds)
|
|
38
36
|
DEFAULT_TIMEOUT = 30
|
|
39
37
|
|
|
38
|
+
# Errors that are safe to retry (transient network issues)
|
|
39
|
+
RETRYABLE_ERRORS = [
|
|
40
|
+
Errno::ECONNREFUSED,
|
|
41
|
+
Errno::ETIMEDOUT,
|
|
42
|
+
Errno::ECONNRESET,
|
|
43
|
+
Net::OpenTimeout,
|
|
44
|
+
Net::ReadTimeout,
|
|
45
|
+
SocketError
|
|
46
|
+
].freeze
|
|
47
|
+
|
|
40
48
|
def current_workers
|
|
41
|
-
with_retry do
|
|
49
|
+
with_retry(RETRYABLE_ERRORS) do
|
|
42
50
|
deployment = apps_client.get_deployment(deployment_name, namespace)
|
|
43
51
|
deployment.spec.replicas
|
|
44
52
|
end
|
|
@@ -52,7 +60,7 @@ module SolidQueueAutoscaler
|
|
|
52
60
|
return quantity
|
|
53
61
|
end
|
|
54
62
|
|
|
55
|
-
with_retry do
|
|
63
|
+
with_retry(RETRYABLE_ERRORS) do
|
|
56
64
|
patch_body = { spec: { replicas: quantity } }
|
|
57
65
|
apps_client.patch_deployment(deployment_name, patch_body, namespace)
|
|
58
66
|
end
|
|
@@ -75,25 +83,6 @@ module SolidQueueAutoscaler
|
|
|
75
83
|
|
|
76
84
|
private
|
|
77
85
|
|
|
78
|
-
# Executes a block with retry logic for transient network errors.
|
|
79
|
-
# Uses exponential backoff: 1s, 2s, 4s delays between retries.
|
|
80
|
-
def with_retry
|
|
81
|
-
attempts = 0
|
|
82
|
-
begin
|
|
83
|
-
attempts += 1
|
|
84
|
-
yield
|
|
85
|
-
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::ECONNRESET,
|
|
86
|
-
Net::OpenTimeout, Net::ReadTimeout, SocketError => e
|
|
87
|
-
if attempts < MAX_RETRIES
|
|
88
|
-
delay = RETRY_DELAYS[attempts - 1] || RETRY_DELAYS.last
|
|
89
|
-
logger&.warn("[Autoscaler] Kubernetes API error (attempt #{attempts}/#{MAX_RETRIES}), retrying in #{delay}s: #{e.message}")
|
|
90
|
-
sleep(delay)
|
|
91
|
-
retry
|
|
92
|
-
end
|
|
93
|
-
raise
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
86
|
def apps_client
|
|
98
87
|
@apps_client ||= build_apps_client
|
|
99
88
|
end
|
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidQueueAutoscaler
|
|
4
4
|
class AutoscaleJob < ActiveJob::Base
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# returns a Proc that isn't evaluated by recurring jobs, causing jobs to
|
|
9
|
-
# go to 'default' queue instead.
|
|
5
|
+
# Default queue - this MUST be set here (not dynamically) because SolidQueue
|
|
6
|
+
# recurring jobs capture the queue name during initialization, BEFORE
|
|
7
|
+
# Rails after_initialize hooks run.
|
|
10
8
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
9
|
+
# The apply_job_settings! method can override this after Rails initializers
|
|
10
|
+
# run, but the default must be set here for SolidQueue recurring to work.
|
|
11
|
+
#
|
|
12
|
+
# You can customize the queue via:
|
|
13
|
+
# config.job_queue = :my_queue
|
|
14
|
+
#
|
|
15
|
+
# For SolidQueue recurring.yml, you can also set queue: directly in the YAML.
|
|
14
16
|
queue_as :autoscaler
|
|
15
17
|
|
|
16
18
|
discard_on ConfigurationError
|
|
@@ -11,6 +11,11 @@ module SolidQueueAutoscaler
|
|
|
11
11
|
# Configuration happens via initializer, nothing to do here
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# After all initializers have run, apply job settings from configuration
|
|
15
|
+
config.after_initialize do
|
|
16
|
+
SolidQueueAutoscaler.apply_job_settings!
|
|
17
|
+
end
|
|
18
|
+
|
|
14
19
|
rake_tasks do
|
|
15
20
|
namespace :solid_queue_autoscaler do
|
|
16
21
|
desc 'Run the autoscaler once for a specific worker (default: :default). Use WORKER=name'
|
|
@@ -100,6 +100,33 @@ module SolidQueueAutoscaler
|
|
|
100
100
|
end
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
+
# Apply job settings (queue, priority) from configuration to AutoscaleJob.
|
|
104
|
+
# Called automatically after Rails initializers run via the railtie.
|
|
105
|
+
# Uses the first configured worker's job_queue/job_priority settings.
|
|
106
|
+
def apply_job_settings!
|
|
107
|
+
return unless defined?(AutoscaleJob)
|
|
108
|
+
return if configurations.empty?
|
|
109
|
+
|
|
110
|
+
# Use the first configured worker's settings
|
|
111
|
+
first_config = configurations.values.first
|
|
112
|
+
job_queue = first_config&.job_queue || :autoscaler
|
|
113
|
+
job_priority = first_config&.job_priority
|
|
114
|
+
|
|
115
|
+
# Set the queue_name class attribute directly (not via queue_as block)
|
|
116
|
+
# This ensures SolidQueue recurring jobs pick up the correct queue
|
|
117
|
+
# Convert to string since ActiveJob internally uses strings for queue names
|
|
118
|
+
AutoscaleJob.queue_name = job_queue.to_s
|
|
119
|
+
|
|
120
|
+
# Set priority if configured
|
|
121
|
+
if job_priority && AutoscaleJob.respond_to?(:priority=)
|
|
122
|
+
AutoscaleJob.priority = job_priority
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
first_config&.logger&.debug(
|
|
126
|
+
"[SolidQueueAutoscaler] AutoscaleJob configured: queue=#{job_queue}, priority=#{job_priority || 'default'}"
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
103
130
|
# Verify the installation is complete and working.
|
|
104
131
|
# Prints a human-friendly report (when verbose: true) and returns a VerificationResult.
|
|
105
132
|
#
|