solid_queue_autoscaler 1.0.0
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 +7 -0
- data/CHANGELOG.md +189 -0
- data/LICENSE.txt +21 -0
- data/README.md +553 -0
- data/lib/generators/solid_queue_autoscaler/dashboard_generator.rb +54 -0
- data/lib/generators/solid_queue_autoscaler/install_generator.rb +21 -0
- data/lib/generators/solid_queue_autoscaler/migration_generator.rb +29 -0
- data/lib/generators/solid_queue_autoscaler/templates/README +41 -0
- data/lib/generators/solid_queue_autoscaler/templates/create_solid_queue_autoscaler_events.rb.erb +24 -0
- data/lib/generators/solid_queue_autoscaler/templates/create_solid_queue_autoscaler_state.rb.erb +15 -0
- data/lib/generators/solid_queue_autoscaler/templates/initializer.rb +58 -0
- data/lib/solid_queue_autoscaler/adapters/base.rb +102 -0
- data/lib/solid_queue_autoscaler/adapters/heroku.rb +93 -0
- data/lib/solid_queue_autoscaler/adapters/kubernetes.rb +158 -0
- data/lib/solid_queue_autoscaler/adapters.rb +57 -0
- data/lib/solid_queue_autoscaler/advisory_lock.rb +71 -0
- data/lib/solid_queue_autoscaler/autoscale_job.rb +71 -0
- data/lib/solid_queue_autoscaler/configuration.rb +269 -0
- data/lib/solid_queue_autoscaler/cooldown_tracker.rb +153 -0
- data/lib/solid_queue_autoscaler/dashboard/engine.rb +136 -0
- data/lib/solid_queue_autoscaler/dashboard/views/layouts/solid_queue_heroku_autoscaler/dashboard/application.html.erb +206 -0
- data/lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/dashboard/index.html.erb +138 -0
- data/lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/events/index.html.erb +102 -0
- data/lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/workers/index.html.erb +106 -0
- data/lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/workers/show.html.erb +209 -0
- data/lib/solid_queue_autoscaler/dashboard.rb +99 -0
- data/lib/solid_queue_autoscaler/decision_engine.rb +228 -0
- data/lib/solid_queue_autoscaler/errors.rb +44 -0
- data/lib/solid_queue_autoscaler/metrics.rb +172 -0
- data/lib/solid_queue_autoscaler/railtie.rb +179 -0
- data/lib/solid_queue_autoscaler/scale_event.rb +292 -0
- data/lib/solid_queue_autoscaler/scaler.rb +294 -0
- data/lib/solid_queue_autoscaler/version.rb +5 -0
- data/lib/solid_queue_autoscaler.rb +108 -0
- metadata +179 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueAutoscaler
|
|
4
|
+
class Scaler
|
|
5
|
+
ScaleResult = Struct.new(
|
|
6
|
+
:success,
|
|
7
|
+
:decision,
|
|
8
|
+
:metrics,
|
|
9
|
+
:error,
|
|
10
|
+
:skipped_reason,
|
|
11
|
+
:executed_at,
|
|
12
|
+
keyword_init: true
|
|
13
|
+
) do
|
|
14
|
+
def success?
|
|
15
|
+
success == true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def skipped?
|
|
19
|
+
!skipped_reason.nil?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def scaled?
|
|
23
|
+
success? && decision && !decision.no_change?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Per-configuration cooldown tracking for multi-worker support
|
|
28
|
+
class << self
|
|
29
|
+
def cooldown_mutex
|
|
30
|
+
@cooldown_mutex ||= Mutex.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def cooldowns
|
|
34
|
+
@cooldowns ||= {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def last_scale_up_at(config_name = :default)
|
|
38
|
+
cooldown_mutex.synchronize { cooldowns.dig(config_name, :scale_up) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def set_last_scale_up_at(config_name, value)
|
|
42
|
+
cooldown_mutex.synchronize do
|
|
43
|
+
cooldowns[config_name] ||= {}
|
|
44
|
+
cooldowns[config_name][:scale_up] = value
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def last_scale_down_at(config_name = :default)
|
|
49
|
+
cooldown_mutex.synchronize { cooldowns.dig(config_name, :scale_down) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def set_last_scale_down_at(config_name, value)
|
|
53
|
+
cooldown_mutex.synchronize do
|
|
54
|
+
cooldowns[config_name] ||= {}
|
|
55
|
+
cooldowns[config_name][:scale_down] = value
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def reset_cooldowns!(config_name = nil)
|
|
60
|
+
cooldown_mutex.synchronize do
|
|
61
|
+
if config_name
|
|
62
|
+
cooldowns.delete(config_name)
|
|
63
|
+
else
|
|
64
|
+
@cooldowns = {}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Backward compatibility setters
|
|
70
|
+
def last_scale_up_at=(value)
|
|
71
|
+
set_last_scale_up_at(:default, value)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def last_scale_down_at=(value)
|
|
75
|
+
set_last_scale_down_at(:default, value)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def initialize(config: nil)
|
|
80
|
+
@config = config || SolidQueueAutoscaler.config
|
|
81
|
+
@lock = AdvisoryLock.new(config: @config)
|
|
82
|
+
@metrics_collector = Metrics.new(config: @config)
|
|
83
|
+
@decision_engine = DecisionEngine.new(config: @config)
|
|
84
|
+
@adapter = @config.adapter
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def run
|
|
88
|
+
return skipped_result('Autoscaler is disabled') unless @config.enabled?
|
|
89
|
+
|
|
90
|
+
return skipped_result('Could not acquire advisory lock (another instance is running)') unless @lock.try_lock
|
|
91
|
+
|
|
92
|
+
begin
|
|
93
|
+
execute_scaling
|
|
94
|
+
ensure
|
|
95
|
+
@lock.release
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def run!
|
|
100
|
+
@lock.with_lock do
|
|
101
|
+
execute_scaling
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def execute_scaling
|
|
108
|
+
metrics = @metrics_collector.collect
|
|
109
|
+
current_workers = @adapter.current_workers
|
|
110
|
+
decision = @decision_engine.decide(metrics: metrics, current_workers: current_workers)
|
|
111
|
+
|
|
112
|
+
log_decision(decision, metrics)
|
|
113
|
+
|
|
114
|
+
return success_result(decision, metrics) if decision.no_change?
|
|
115
|
+
|
|
116
|
+
if cooldown_active?(decision)
|
|
117
|
+
remaining = cooldown_remaining(decision)
|
|
118
|
+
return skipped_result("Cooldown active (#{remaining.round}s remaining)", decision: decision, metrics: metrics)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
apply_decision(decision, metrics)
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
error_result(e)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def apply_decision(decision, metrics)
|
|
127
|
+
@adapter.scale(decision.to)
|
|
128
|
+
record_scale_time(decision)
|
|
129
|
+
record_scale_event(decision, metrics)
|
|
130
|
+
|
|
131
|
+
log_scale_action(decision)
|
|
132
|
+
|
|
133
|
+
success_result(decision, metrics)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def cooldown_active?(decision)
|
|
137
|
+
config_name = @config.name
|
|
138
|
+
if decision.scale_up?
|
|
139
|
+
last_scale_up = self.class.last_scale_up_at(config_name)
|
|
140
|
+
return false unless last_scale_up
|
|
141
|
+
|
|
142
|
+
Time.current - last_scale_up < @config.effective_scale_up_cooldown
|
|
143
|
+
elsif decision.scale_down?
|
|
144
|
+
last_scale_down = self.class.last_scale_down_at(config_name)
|
|
145
|
+
return false unless last_scale_down
|
|
146
|
+
|
|
147
|
+
Time.current - last_scale_down < @config.effective_scale_down_cooldown
|
|
148
|
+
else
|
|
149
|
+
false
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def cooldown_remaining(decision)
|
|
154
|
+
config_name = @config.name
|
|
155
|
+
if decision.scale_up?
|
|
156
|
+
elapsed = Time.current - self.class.last_scale_up_at(config_name)
|
|
157
|
+
@config.effective_scale_up_cooldown - elapsed
|
|
158
|
+
else
|
|
159
|
+
elapsed = Time.current - self.class.last_scale_down_at(config_name)
|
|
160
|
+
@config.effective_scale_down_cooldown - elapsed
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def record_scale_time(decision)
|
|
165
|
+
config_name = @config.name
|
|
166
|
+
if decision.scale_up?
|
|
167
|
+
self.class.set_last_scale_up_at(config_name, Time.current)
|
|
168
|
+
elsif decision.scale_down?
|
|
169
|
+
self.class.set_last_scale_down_at(config_name, Time.current)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def log_decision(decision, metrics)
|
|
174
|
+
worker_label = @config.name == :default ? '' : "[#{@config.name}] "
|
|
175
|
+
logger.info(
|
|
176
|
+
"[Autoscaler] #{worker_label}Evaluated: action=#{decision.action} " \
|
|
177
|
+
"workers=#{decision.from}->#{decision.to} " \
|
|
178
|
+
"queue_depth=#{metrics.queue_depth} " \
|
|
179
|
+
"latency=#{metrics.oldest_job_age_seconds.round}s " \
|
|
180
|
+
"reason=\"#{decision.reason}\""
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def log_scale_action(decision)
|
|
185
|
+
prefix = @config.dry_run? ? '[DRY RUN] ' : ''
|
|
186
|
+
worker_label = @config.name == :default ? '' : "[#{@config.name}] "
|
|
187
|
+
logger.info(
|
|
188
|
+
"#{prefix}[Autoscaler] #{worker_label}Scaling #{decision.action}: " \
|
|
189
|
+
"#{decision.from} -> #{decision.to} workers (#{decision.reason})"
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def success_result(decision, metrics)
|
|
194
|
+
# Record no_change events if configured
|
|
195
|
+
record_scale_event(decision, metrics) if decision&.no_change? && @config.record_all_events?
|
|
196
|
+
|
|
197
|
+
ScaleResult.new(
|
|
198
|
+
success: true,
|
|
199
|
+
decision: decision,
|
|
200
|
+
metrics: metrics,
|
|
201
|
+
executed_at: Time.current
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def skipped_result(reason, decision: nil, metrics: nil)
|
|
206
|
+
logger.debug("[Autoscaler] Skipped: #{reason}")
|
|
207
|
+
|
|
208
|
+
# Record skipped events
|
|
209
|
+
record_skipped_event(reason, decision, metrics)
|
|
210
|
+
|
|
211
|
+
ScaleResult.new(
|
|
212
|
+
success: true,
|
|
213
|
+
decision: decision,
|
|
214
|
+
metrics: metrics,
|
|
215
|
+
skipped_reason: reason,
|
|
216
|
+
executed_at: Time.current
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def error_result(error)
|
|
221
|
+
logger.error("[Autoscaler] Error: #{error.class}: #{error.message}")
|
|
222
|
+
|
|
223
|
+
# Record error events
|
|
224
|
+
record_error_event(error)
|
|
225
|
+
|
|
226
|
+
ScaleResult.new(
|
|
227
|
+
success: false,
|
|
228
|
+
error: error,
|
|
229
|
+
executed_at: Time.current
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def logger
|
|
234
|
+
@config.logger
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def record_scale_event(decision, metrics)
|
|
238
|
+
return unless @config.record_events?
|
|
239
|
+
|
|
240
|
+
ScaleEvent.create!(
|
|
241
|
+
{
|
|
242
|
+
worker_name: @config.name.to_s,
|
|
243
|
+
action: decision.action.to_s,
|
|
244
|
+
from_workers: decision.from,
|
|
245
|
+
to_workers: decision.to,
|
|
246
|
+
reason: decision.reason,
|
|
247
|
+
queue_depth: metrics&.queue_depth || 0,
|
|
248
|
+
latency_seconds: metrics&.oldest_job_age_seconds || 0.0,
|
|
249
|
+
metrics_json: metrics&.to_h&.to_json,
|
|
250
|
+
dry_run: @config.dry_run?
|
|
251
|
+
},
|
|
252
|
+
connection: @config.connection
|
|
253
|
+
)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def record_skipped_event(reason, decision, metrics)
|
|
257
|
+
return unless @config.record_events?
|
|
258
|
+
|
|
259
|
+
ScaleEvent.create!(
|
|
260
|
+
{
|
|
261
|
+
worker_name: @config.name.to_s,
|
|
262
|
+
action: 'skipped',
|
|
263
|
+
from_workers: decision&.from || 0,
|
|
264
|
+
to_workers: decision&.to || 0,
|
|
265
|
+
reason: reason,
|
|
266
|
+
queue_depth: metrics&.queue_depth || 0,
|
|
267
|
+
latency_seconds: metrics&.oldest_job_age_seconds || 0.0,
|
|
268
|
+
metrics_json: metrics&.to_h&.to_json,
|
|
269
|
+
dry_run: @config.dry_run?
|
|
270
|
+
},
|
|
271
|
+
connection: @config.connection
|
|
272
|
+
)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def record_error_event(error)
|
|
276
|
+
return unless @config.record_events?
|
|
277
|
+
|
|
278
|
+
ScaleEvent.create!(
|
|
279
|
+
{
|
|
280
|
+
worker_name: @config.name.to_s,
|
|
281
|
+
action: 'error',
|
|
282
|
+
from_workers: 0,
|
|
283
|
+
to_workers: 0,
|
|
284
|
+
reason: "#{error.class}: #{error.message}",
|
|
285
|
+
queue_depth: 0,
|
|
286
|
+
latency_seconds: 0.0,
|
|
287
|
+
metrics_json: nil,
|
|
288
|
+
dry_run: @config.dry_run?
|
|
289
|
+
},
|
|
290
|
+
connection: @config.connection
|
|
291
|
+
)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_record'
|
|
4
|
+
require 'active_support'
|
|
5
|
+
require 'active_support/core_ext/numeric/time'
|
|
6
|
+
|
|
7
|
+
require_relative 'solid_queue_autoscaler/version'
|
|
8
|
+
require_relative 'solid_queue_autoscaler/errors'
|
|
9
|
+
require_relative 'solid_queue_autoscaler/adapters'
|
|
10
|
+
require_relative 'solid_queue_autoscaler/configuration'
|
|
11
|
+
require_relative 'solid_queue_autoscaler/advisory_lock'
|
|
12
|
+
require_relative 'solid_queue_autoscaler/metrics'
|
|
13
|
+
require_relative 'solid_queue_autoscaler/decision_engine'
|
|
14
|
+
require_relative 'solid_queue_autoscaler/cooldown_tracker'
|
|
15
|
+
require_relative 'solid_queue_autoscaler/scale_event'
|
|
16
|
+
require_relative 'solid_queue_autoscaler/scaler'
|
|
17
|
+
|
|
18
|
+
module SolidQueueAutoscaler
|
|
19
|
+
class << self
|
|
20
|
+
# Registry of named configurations for multi-worker support
|
|
21
|
+
def configurations
|
|
22
|
+
@configurations ||= {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Configure a named worker type (default: :default for backward compatibility)
|
|
26
|
+
# @param name [Symbol] The name of the worker type (e.g., :critical_worker, :default_worker)
|
|
27
|
+
# @yield [Configuration] The configuration object to customize
|
|
28
|
+
# @return [Configuration] The configured configuration object
|
|
29
|
+
def configure(name = :default)
|
|
30
|
+
config_obj = configurations[name] ||= Configuration.new
|
|
31
|
+
config_obj.name = name
|
|
32
|
+
yield(config_obj) if block_given?
|
|
33
|
+
config_obj.validate!
|
|
34
|
+
config_obj
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get configuration for a named worker type
|
|
38
|
+
# @param name [Symbol] The name of the worker type
|
|
39
|
+
# @return [Configuration] The configuration object
|
|
40
|
+
def config(name = :default)
|
|
41
|
+
configurations[name] || configure(name)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Scale a specific worker type
|
|
45
|
+
# @param name [Symbol] The name of the worker type to scale
|
|
46
|
+
# @return [Scaler::ScaleResult] The result of the scaling operation
|
|
47
|
+
def scale!(name = :default)
|
|
48
|
+
Scaler.new(config: config(name)).run
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Scale all configured worker types
|
|
52
|
+
# @return [Hash<Symbol, Scaler::ScaleResult>] Results keyed by worker name
|
|
53
|
+
def scale_all!
|
|
54
|
+
return {} if configurations.empty?
|
|
55
|
+
|
|
56
|
+
# Copy keys to avoid modifying hash during iteration
|
|
57
|
+
worker_names = configurations.keys.dup
|
|
58
|
+
worker_names.each_with_object({}) do |name, results|
|
|
59
|
+
results[name] = scale!(name)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get metrics for a specific worker type
|
|
64
|
+
# @param name [Symbol] The name of the worker type
|
|
65
|
+
# @return [Metrics::Result] The collected metrics
|
|
66
|
+
def metrics(name = :default)
|
|
67
|
+
Metrics.new(config: config(name)).collect
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Get current worker count for a specific worker type
|
|
71
|
+
# @param name [Symbol] The name of the worker type
|
|
72
|
+
# @return [Integer] The current number of workers
|
|
73
|
+
def current_workers(name = :default)
|
|
74
|
+
config(name).adapter.current_workers
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# List all registered worker type names
|
|
78
|
+
# @return [Array<Symbol>] List of configured worker names
|
|
79
|
+
def registered_workers
|
|
80
|
+
configurations.keys
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Reset all configurations (useful for testing)
|
|
84
|
+
def reset_configuration!
|
|
85
|
+
@configurations = {}
|
|
86
|
+
Scaler.reset_cooldowns!
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Backward compatibility: single configuration accessor
|
|
90
|
+
def configuration
|
|
91
|
+
configurations[:default]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def configuration=(config_obj)
|
|
95
|
+
if config_obj.nil?
|
|
96
|
+
@configurations = {}
|
|
97
|
+
else
|
|
98
|
+
config_obj.name ||= :default
|
|
99
|
+
configurations[:default] = config_obj
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
require_relative 'solid_queue_autoscaler/railtie' if defined?(Rails::Railtie)
|
|
106
|
+
require_relative 'solid_queue_autoscaler/dashboard'
|
|
107
|
+
|
|
108
|
+
require_relative 'solid_queue_autoscaler/autoscale_job' if defined?(ActiveJob::Base)
|
metadata
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: solid_queue_autoscaler
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- reillyse
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-01-16 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activerecord
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: activesupport
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '7.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '7.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: platform-api
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rake
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '13.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '13.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rspec
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.12'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.12'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rubocop
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '1.50'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '1.50'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: webmock
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '3.18'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '3.18'
|
|
111
|
+
description: A control plane for Solid Queue on Heroku that automatically scales worker
|
|
112
|
+
dynos based on queue depth, job latency, and throughput. Uses PostgreSQL advisory
|
|
113
|
+
locks for singleton behavior and the Heroku Platform API for scaling.
|
|
114
|
+
email: []
|
|
115
|
+
executables: []
|
|
116
|
+
extensions: []
|
|
117
|
+
extra_rdoc_files: []
|
|
118
|
+
files:
|
|
119
|
+
- CHANGELOG.md
|
|
120
|
+
- LICENSE.txt
|
|
121
|
+
- README.md
|
|
122
|
+
- lib/generators/solid_queue_autoscaler/dashboard_generator.rb
|
|
123
|
+
- lib/generators/solid_queue_autoscaler/install_generator.rb
|
|
124
|
+
- lib/generators/solid_queue_autoscaler/migration_generator.rb
|
|
125
|
+
- lib/generators/solid_queue_autoscaler/templates/README
|
|
126
|
+
- lib/generators/solid_queue_autoscaler/templates/create_solid_queue_autoscaler_events.rb.erb
|
|
127
|
+
- lib/generators/solid_queue_autoscaler/templates/create_solid_queue_autoscaler_state.rb.erb
|
|
128
|
+
- lib/generators/solid_queue_autoscaler/templates/initializer.rb
|
|
129
|
+
- lib/solid_queue_autoscaler.rb
|
|
130
|
+
- lib/solid_queue_autoscaler/adapters.rb
|
|
131
|
+
- lib/solid_queue_autoscaler/adapters/base.rb
|
|
132
|
+
- lib/solid_queue_autoscaler/adapters/heroku.rb
|
|
133
|
+
- lib/solid_queue_autoscaler/adapters/kubernetes.rb
|
|
134
|
+
- lib/solid_queue_autoscaler/advisory_lock.rb
|
|
135
|
+
- lib/solid_queue_autoscaler/autoscale_job.rb
|
|
136
|
+
- lib/solid_queue_autoscaler/configuration.rb
|
|
137
|
+
- lib/solid_queue_autoscaler/cooldown_tracker.rb
|
|
138
|
+
- lib/solid_queue_autoscaler/dashboard.rb
|
|
139
|
+
- lib/solid_queue_autoscaler/dashboard/engine.rb
|
|
140
|
+
- lib/solid_queue_autoscaler/dashboard/views/layouts/solid_queue_heroku_autoscaler/dashboard/application.html.erb
|
|
141
|
+
- lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/dashboard/index.html.erb
|
|
142
|
+
- lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/events/index.html.erb
|
|
143
|
+
- lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/workers/index.html.erb
|
|
144
|
+
- lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/workers/show.html.erb
|
|
145
|
+
- lib/solid_queue_autoscaler/decision_engine.rb
|
|
146
|
+
- lib/solid_queue_autoscaler/errors.rb
|
|
147
|
+
- lib/solid_queue_autoscaler/metrics.rb
|
|
148
|
+
- lib/solid_queue_autoscaler/railtie.rb
|
|
149
|
+
- lib/solid_queue_autoscaler/scale_event.rb
|
|
150
|
+
- lib/solid_queue_autoscaler/scaler.rb
|
|
151
|
+
- lib/solid_queue_autoscaler/version.rb
|
|
152
|
+
homepage: https://github.com/reillyse/solid_queue_autoscaler
|
|
153
|
+
licenses:
|
|
154
|
+
- MIT
|
|
155
|
+
metadata:
|
|
156
|
+
homepage_uri: https://github.com/reillyse/solid_queue_autoscaler
|
|
157
|
+
source_code_uri: https://github.com/reillyse/solid_queue_autoscaler
|
|
158
|
+
changelog_uri: https://github.com/reillyse/solid_queue_autoscaler/blob/main/CHANGELOG.md
|
|
159
|
+
rubygems_mfa_required: 'true'
|
|
160
|
+
post_install_message:
|
|
161
|
+
rdoc_options: []
|
|
162
|
+
require_paths:
|
|
163
|
+
- lib
|
|
164
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
165
|
+
requirements:
|
|
166
|
+
- - ">="
|
|
167
|
+
- !ruby/object:Gem::Version
|
|
168
|
+
version: 3.1.0
|
|
169
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
170
|
+
requirements:
|
|
171
|
+
- - ">="
|
|
172
|
+
- !ruby/object:Gem::Version
|
|
173
|
+
version: '0'
|
|
174
|
+
requirements: []
|
|
175
|
+
rubygems_version: 3.5.22
|
|
176
|
+
signing_key:
|
|
177
|
+
specification_version: 4
|
|
178
|
+
summary: Auto-scale Solid Queue workers on Heroku based on queue metrics
|
|
179
|
+
test_files: []
|