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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +189 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +553 -0
  5. data/lib/generators/solid_queue_autoscaler/dashboard_generator.rb +54 -0
  6. data/lib/generators/solid_queue_autoscaler/install_generator.rb +21 -0
  7. data/lib/generators/solid_queue_autoscaler/migration_generator.rb +29 -0
  8. data/lib/generators/solid_queue_autoscaler/templates/README +41 -0
  9. data/lib/generators/solid_queue_autoscaler/templates/create_solid_queue_autoscaler_events.rb.erb +24 -0
  10. data/lib/generators/solid_queue_autoscaler/templates/create_solid_queue_autoscaler_state.rb.erb +15 -0
  11. data/lib/generators/solid_queue_autoscaler/templates/initializer.rb +58 -0
  12. data/lib/solid_queue_autoscaler/adapters/base.rb +102 -0
  13. data/lib/solid_queue_autoscaler/adapters/heroku.rb +93 -0
  14. data/lib/solid_queue_autoscaler/adapters/kubernetes.rb +158 -0
  15. data/lib/solid_queue_autoscaler/adapters.rb +57 -0
  16. data/lib/solid_queue_autoscaler/advisory_lock.rb +71 -0
  17. data/lib/solid_queue_autoscaler/autoscale_job.rb +71 -0
  18. data/lib/solid_queue_autoscaler/configuration.rb +269 -0
  19. data/lib/solid_queue_autoscaler/cooldown_tracker.rb +153 -0
  20. data/lib/solid_queue_autoscaler/dashboard/engine.rb +136 -0
  21. data/lib/solid_queue_autoscaler/dashboard/views/layouts/solid_queue_heroku_autoscaler/dashboard/application.html.erb +206 -0
  22. data/lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/dashboard/index.html.erb +138 -0
  23. data/lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/events/index.html.erb +102 -0
  24. data/lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/workers/index.html.erb +106 -0
  25. data/lib/solid_queue_autoscaler/dashboard/views/solid_queue_heroku_autoscaler/dashboard/workers/show.html.erb +209 -0
  26. data/lib/solid_queue_autoscaler/dashboard.rb +99 -0
  27. data/lib/solid_queue_autoscaler/decision_engine.rb +228 -0
  28. data/lib/solid_queue_autoscaler/errors.rb +44 -0
  29. data/lib/solid_queue_autoscaler/metrics.rb +172 -0
  30. data/lib/solid_queue_autoscaler/railtie.rb +179 -0
  31. data/lib/solid_queue_autoscaler/scale_event.rb +292 -0
  32. data/lib/solid_queue_autoscaler/scaler.rb +294 -0
  33. data/lib/solid_queue_autoscaler/version.rb +5 -0
  34. data/lib/solid_queue_autoscaler.rb +108 -0
  35. 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueAutoscaler
4
+ VERSION = '1.0.0'
5
+ 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: []