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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf794daeb74474c136c8aec706793bf74617dcb610abf42df89ddb4fefd99274
4
- data.tar.gz: 96ec9ad6993871c7773ff524c5d71ff3e919ff89aacba5587d5dee63aa277f5d
3
+ metadata.gz: bf4f38fa3806f153c03715b02554d1a82837da595fb9dadf4fb8063d6f52b3c8
4
+ data.tar.gz: 3737a7de81ab147dbd8e38fc87a2f601c21ea4d895758418f4bd484b0483ea12
5
5
  SHA512:
6
- metadata.gz: 0b8dd105d028035aee534300ee1d91af9193faefe4e27c8ba3d72c98b3c401cdc52a04894bdeef0b14ebdd7a42f120e563bb378509f2a9ac046a18554c7079d0
7
- data.tar.gz: 9980ac5a53affb82b264cd9e7c9bacb388d26a5ecc0116fdd1e22ce527c66ec63bc51d5fe47a492df1941df1b4d0052cfed721e7e4b62566a5c70ab885852f9c
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
- # IMPORTANT: Use a static queue name so SolidQueue recurring jobs work correctly.
6
- # When using SolidQueue recurring.yml without specifying queue:, SolidQueue
7
- # checks the job class's queue_name attribute. A dynamic queue_as block
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
- # To use a custom queue:
12
- # 1. Set queue: in your recurring.yml (recommended)
13
- # 2. Or use AutoscaleJob.set(queue: :my_queue).perform_later
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'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueueAutoscaler
4
- VERSION = '1.0.11'
4
+ VERSION = '1.0.13'
5
5
  end
@@ -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
  #
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue_autoscaler
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.11
4
+ version: 1.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - reillyse