solid_queue_autoscaler 1.0.19 → 1.0.21

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: 8a4488b919923025829a72e3c9a23220a528f76499adfe1575b3f7484adca8ec
4
- data.tar.gz: 19b2bf974f03231b8f060ebaaa12ff0ce214ee3bb89620a1b90981fee3e0410e
3
+ metadata.gz: 4d595bb4e0a25ed4dee60b1356275872000b56845d574a2df7251a3c49f51a2c
4
+ data.tar.gz: 3fa34bfdbd30991096ceba918548660b955ab882ca050bae2e03be94f19c0967
5
5
  SHA512:
6
- metadata.gz: 4dac7f9c83dab082137824c6a730fb6bb68e042758f155aac8480067810904bc234b9f152c7ee023264b6fcb4eb09c2fbfebc43b668520b0fdb446cc66eb1dad
7
- data.tar.gz: 56c9cc4d51523f1752fc36e70f8c3cc4cb83177c4e7c345c8972116d27b0e12812aedbe2839f8bcb958f4ea50dd20432f3a686d2e9217e2fbf2588bf0cbfe872
6
+ metadata.gz: 52e7e13ad3261de3ca958e5e5468b9fde784c1573a4e3865022331c07e21ce74d047646cb68f7d9376df1b3863ffded3a2dcd649655047ca240db074050f65c0
7
+ data.tar.gz: ce1ad6a0ab0eb9205cf73db618d0363afeedc449c9527b0187f634aa320ec0735e4c216990f0dc3aae760f47936ab48abd5ab278aafd845b4e3d0ecd48f9a923
data/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.21] - 2025-02-02
11
+
12
+ ### Added
13
+ - **Scale-from-zero documentation** - Updated README and docs/configuration.md with:
14
+ - New "Faster Scale-from-Zero" section explaining the v1.0.20 optimizations
15
+ - Configuration reference for `scale_from_zero_queue_depth` and `scale_from_zero_latency_seconds`
16
+ - Example configuration showing how to customize scale-from-zero behavior
17
+ - Explanation of cooldown bypass and grace period for other workers
18
+
19
+ ## [1.0.20] - 2025-02-02
20
+
21
+ ### Added
22
+ - **Scale-from-zero optimization** - New configuration options for faster cold starts when `min_workers = 0`:
23
+ - `scale_from_zero_queue_depth` (default: 1) - Scale up immediately when at 0 workers if queue has at least this many jobs
24
+ - `scale_from_zero_latency_seconds` (default: 1.0) - Job must be at least this old before scaling up (gives other workers a chance to pick it up first)
25
+ - When at 0 workers, uses these lower thresholds instead of the normal `scale_up_queue_depth` and `scale_up_latency_seconds`
26
+ - Cooldowns are bypassed when scaling from 0 workers for fast cold start
27
+ - Comprehensive tests in `scale_to_zero_workflow_spec.rb`
28
+
10
29
  ## [1.0.19] - 2025-02-02
11
30
 
12
31
  ### Added
data/README.md CHANGED
@@ -80,6 +80,41 @@ end
80
80
 
81
81
  Total cold-start time is typically **30-90 seconds** depending on your configuration and dyno startup time.
82
82
 
83
+ ### Faster Scale-from-Zero (v1.0.20+)
84
+
85
+ As of **v1.0.20**, the autoscaler includes optimizations for faster cold starts when scaling from zero:
86
+
87
+ 1. **Lower thresholds at zero**: When workers are at 0 (with `min_workers = 0`), the autoscaler uses separate, more aggressive thresholds:
88
+ - `scale_from_zero_queue_depth` (default: 1) - Scale up when there's at least 1 job
89
+ - `scale_from_zero_latency_seconds` (default: 1.0) - Job must be at least 1 second old
90
+
91
+ 2. **Cooldown bypass**: Cooldowns are skipped when scaling from 0 workers, ensuring the fastest possible response.
92
+
93
+ 3. **Grace period for other workers**: The `scale_from_zero_latency_seconds` setting (default: 1 second) ensures that if you have multiple worker types, other workers have a brief chance to pick up the job before a new dyno is spun up.
94
+
95
+ **Example configuration:**
96
+
97
+ ```ruby
98
+ SolidQueueAutoscaler.configure(:batch_worker) do |config|
99
+ config.adapter = :heroku
100
+ config.heroku_api_key = ENV['HEROKU_API_KEY']
101
+ config.heroku_app_name = ENV['HEROKU_APP_NAME']
102
+ config.process_type = 'batch_worker'
103
+
104
+ # Enable scale-to-zero
105
+ config.min_workers = 0
106
+ config.max_workers = 5
107
+
108
+ # Normal scaling thresholds (used when workers > 0)
109
+ config.scale_up_queue_depth = 100
110
+ config.scale_up_latency_seconds = 300
111
+
112
+ # Scale-from-zero thresholds (used when workers == 0)
113
+ config.scale_from_zero_queue_depth = 1 # Scale up with just 1 job
114
+ config.scale_from_zero_latency_seconds = 2.0 # Wait 2 seconds for other workers
115
+ end
116
+ ```
117
+
83
118
  **Where to run the autoscaler**: The autoscaler job **must run on a process that's always running** (like your web dyno), NOT on the workers being scaled. If the autoscaler runs on workers and those workers scale to zero, there's nothing to scale them back up!
84
119
 
85
120
  ```yaml
@@ -326,6 +361,17 @@ Scaling down triggers when **ALL** thresholds are met:
326
361
  | `scale_down_cooldown_seconds` | Integer | `nil` | Override for scale-down cooldown |
327
362
  | `persist_cooldowns` | Boolean | `true` | Save cooldowns to database |
328
363
 
364
+ ### Scale-from-Zero Optimization
365
+
366
+ These settings control the faster cold-start behavior when `min_workers = 0` and workers are currently at 0:
367
+
368
+ | Option | Type | Default | Description |
369
+ |--------|------|---------|-------------|
370
+ | `scale_from_zero_queue_depth` | Integer | `1` | Jobs in queue to trigger scale-up when at 0 workers |
371
+ | `scale_from_zero_latency_seconds` | Float | `1.0` | Job must be at least this old (gives other workers a chance) |
372
+
373
+ **Note:** When scaling from 0 workers, cooldowns are automatically bypassed for the fastest possible response.
374
+
329
375
  ### AutoscaleJob Settings
330
376
 
331
377
  | Option | Type | Default | Description |
@@ -69,6 +69,9 @@ module SolidQueueAutoscaler
69
69
  # AutoscaleJob settings
70
70
  attr_accessor :job_queue, :job_priority
71
71
 
72
+ # Scale-from-zero settings (for faster cold start when min_workers=0)
73
+ attr_accessor :scale_from_zero_queue_depth, :scale_from_zero_latency_seconds
74
+
72
75
  def initialize
73
76
  # Configuration name (auto-set when using named configurations)
74
77
  @name = :default
@@ -141,6 +144,11 @@ module SolidQueueAutoscaler
141
144
  # AutoscaleJob settings
142
145
  @job_queue = :autoscaler # Queue name for the autoscaler job
143
146
  @job_priority = nil # Job priority (lower = higher priority, nil = default)
147
+
148
+ # Scale-from-zero settings (for faster cold start when min_workers=0)
149
+ # When at 0 workers, use these lower thresholds instead of normal scale_up thresholds
150
+ @scale_from_zero_queue_depth = 1 # Scale up if at least 1 job in queue
151
+ @scale_from_zero_latency_seconds = 1.0 # Job must be at least 1 second old (gives other workers a chance)
144
152
  end
145
153
 
146
154
  # Returns the lock key, auto-generating based on name if not explicitly set
@@ -196,6 +204,10 @@ module SolidQueueAutoscaler
196
204
  errors << "scaling_strategy must be one of: #{VALID_SCALING_STRATEGIES.join(', ')}"
197
205
  end
198
206
 
207
+ # Validate scale-from-zero settings
208
+ errors << 'scale_from_zero_queue_depth must be > 0' if scale_from_zero_queue_depth <= 0
209
+ errors << 'scale_from_zero_latency_seconds must be >= 0' if scale_from_zero_latency_seconds.negative?
210
+
199
211
  raise ConfigurationError, errors.join(', ') if errors.any?
200
212
 
201
213
  true
@@ -41,12 +41,30 @@ module SolidQueueAutoscaler
41
41
  def should_scale_up?(metrics, current_workers)
42
42
  return false if current_workers >= @config.max_workers
43
43
 
44
+ # Special case: scale-from-zero uses lower thresholds for faster cold start
45
+ # This allows immediate scaling when at 0 workers with any work in queue
46
+ if current_workers.zero? && @config.min_workers.zero?
47
+ return should_scale_from_zero?(metrics)
48
+ end
49
+
44
50
  queue_depth_high = metrics.queue_depth >= @config.scale_up_queue_depth
45
51
  latency_high = metrics.oldest_job_age_seconds >= @config.scale_up_latency_seconds
46
52
 
47
53
  queue_depth_high || latency_high
48
54
  end
49
55
 
56
+ # Scale-from-zero check: uses lower thresholds for faster cold start
57
+ # Requires:
58
+ # 1. Queue depth >= scale_from_zero_queue_depth (default: 1)
59
+ # 2. Oldest job age >= scale_from_zero_latency_seconds (default: 1s)
60
+ # This gives other workers/queues a chance to pick up the job first
61
+ def should_scale_from_zero?(metrics)
62
+ has_work = metrics.queue_depth >= @config.scale_from_zero_queue_depth
63
+ job_old_enough = metrics.oldest_job_age_seconds >= @config.scale_from_zero_latency_seconds
64
+
65
+ has_work && job_old_enough
66
+ end
67
+
50
68
  def should_scale_down?(metrics, current_workers)
51
69
  return false if current_workers <= @config.min_workers
52
70
 
@@ -161,12 +179,22 @@ module SolidQueueAutoscaler
161
179
  def build_scale_up_reason(metrics, current_workers = nil, target = nil)
162
180
  reasons = []
163
181
 
164
- if metrics.queue_depth >= @config.scale_up_queue_depth
165
- reasons << "queue_depth=#{metrics.queue_depth} >= #{@config.scale_up_queue_depth}"
166
- end
182
+ # Check if this is a scale-from-zero scenario
183
+ is_scale_from_zero = current_workers&.zero? && @config.min_workers.zero? &&
184
+ metrics.queue_depth >= @config.scale_from_zero_queue_depth &&
185
+ metrics.oldest_job_age_seconds >= @config.scale_from_zero_latency_seconds
167
186
 
168
- if metrics.oldest_job_age_seconds >= @config.scale_up_latency_seconds
169
- reasons << "latency=#{metrics.oldest_job_age_seconds.round}s >= #{@config.scale_up_latency_seconds}s"
187
+ if is_scale_from_zero
188
+ reasons << "scale_from_zero: queue_depth=#{metrics.queue_depth} >= #{@config.scale_from_zero_queue_depth}"
189
+ reasons << "job_age=#{metrics.oldest_job_age_seconds.round(1)}s >= #{@config.scale_from_zero_latency_seconds}s"
190
+ else
191
+ if metrics.queue_depth >= @config.scale_up_queue_depth
192
+ reasons << "queue_depth=#{metrics.queue_depth} >= #{@config.scale_up_queue_depth}"
193
+ end
194
+
195
+ if metrics.oldest_job_age_seconds >= @config.scale_up_latency_seconds
196
+ reasons << "latency=#{metrics.oldest_job_age_seconds.round}s >= #{@config.scale_up_latency_seconds}s"
197
+ end
170
198
  end
171
199
 
172
200
  base_reason = reasons.join(', ')
@@ -185,6 +185,12 @@ module SolidQueueAutoscaler
185
185
  end
186
186
 
187
187
  def cooldown_active?(decision)
188
+ # Bypass cooldowns when scaling from zero - we want fast cold start
189
+ # This is safe because there are no workers to destabilize
190
+ if decision.scale_up? && decision.from.zero? && @config.min_workers.zero?
191
+ return false
192
+ end
193
+
188
194
  if @config.persist_cooldowns && cooldown_tracker.table_exists?
189
195
  # Use database-persisted cooldowns (survives process restarts)
190
196
  if decision.scale_up?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueueAutoscaler
4
- VERSION = '1.0.19'
4
+ VERSION = '1.0.21'
5
5
  end
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.19
4
+ version: 1.0.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - reillyse