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 +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +46 -0
- data/lib/solid_queue_autoscaler/configuration.rb +12 -0
- data/lib/solid_queue_autoscaler/decision_engine.rb +33 -5
- data/lib/solid_queue_autoscaler/scaler.rb +6 -0
- data/lib/solid_queue_autoscaler/version.rb +1 -1
- 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: 4d595bb4e0a25ed4dee60b1356275872000b56845d574a2df7251a3c49f51a2c
|
|
4
|
+
data.tar.gz: 3fa34bfdbd30991096ceba918548660b955ab882ca050bae2e03be94f19c0967
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
169
|
-
reasons << "
|
|
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?
|