sidekiq_autoscale 0.2.2
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/MIT-LICENSE +20 -0
- data/README.md +152 -0
- data/Rakefile +29 -0
- data/lib/generators/sidekiq_autoscale/install/install_generator.rb +13 -0
- data/lib/generators/sidekiq_autoscale/install/templates/sidekiq_autoscale_initializer_template.template +53 -0
- data/lib/sidekiq_autoscale/adapters/heroku_adapter.rb +31 -0
- data/lib/sidekiq_autoscale/adapters/kubernetes_adapter.rb +32 -0
- data/lib/sidekiq_autoscale/adapters/nil_adapter.rb +17 -0
- data/lib/sidekiq_autoscale/config/callbacks.rb +0 -0
- data/lib/sidekiq_autoscale/config/shared_configs.rb +201 -0
- data/lib/sidekiq_autoscale/exception.rb +6 -0
- data/lib/sidekiq_autoscale/middleware.rb +72 -0
- data/lib/sidekiq_autoscale/railtie.rb +19 -0
- data/lib/sidekiq_autoscale/sidekiq_interface.rb +45 -0
- data/lib/sidekiq_autoscale/strategies/base_scaling.rb +28 -0
- data/lib/sidekiq_autoscale/strategies/delay_scaling.rb +72 -0
- data/lib/sidekiq_autoscale/strategies/dynamic_latency_scaling.rb +47 -0
- data/lib/sidekiq_autoscale/strategies/linear_scaling.rb +44 -0
- data/lib/sidekiq_autoscale/strategies/oldest_job_scaling.rb +36 -0
- data/lib/sidekiq_autoscale/version.rb +5 -0
- data/lib/sidekiq_autoscale.rb +30 -0
- data/lib/tasks/sidekiq_autoscale_tasks.rake +5 -0
- metadata +365 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: db767100bab6dcb54dcea794f7a5fb3b4e1ba925330ece323350fd6256399899
|
4
|
+
data.tar.gz: 4bb7d27d12ec1953e3cfdc1b37cccabbde656e444abe8e6cf4ecafad838e9f94
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4bd8d6c970aac521f6cfc52bcb4fc8834058ab2bb23e68bf903eb51e8480e0c3b40b69bda22955e1466985d77a57067b94916ba61b10220de1d3836a0c3dd70f
|
7
|
+
data.tar.gz: a4818632e8f52a4955beb4014f113f3b5f6f64f38c9f6503229a60b97fae38e77361b99fb1297b8590e62fcacc395f9f9fe570357a46f600ef31033bd72aca6e
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2019 Traction Guest
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
[](https://codeclimate.com/github/tractionguest/sidekiq_autoscaling/maintainability)
|
2
|
+
|
3
|
+
[](https://codeclimate.com/github/tractionguest/sidekiq_autoscaling/test_coverage)
|
4
|
+
|
5
|
+
# SidekiqAutoscale
|
6
|
+
|
7
|
+
A simple gem to manage autoscaling of Sidekiq worker pools
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem 'sidekiq_autoscale'
|
18
|
+
```
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
```bash
|
22
|
+
$ bundle install
|
23
|
+
```
|
24
|
+
|
25
|
+
Install the initializer:
|
26
|
+
```bash
|
27
|
+
$ rails g sidekiq_autoscale:install
|
28
|
+
```
|
29
|
+
|
30
|
+
This will create a `config/initializers/sidekiq_autoscale.rb` file.
|
31
|
+
|
32
|
+
### Installing middleware in Sidekiq chain
|
33
|
+
|
34
|
+
Add the scaling middleware to your Sidekiq configuration
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
Sidekiq.configure_server do |config|
|
38
|
+
.....
|
39
|
+
config.server_middleware do |chain|
|
40
|
+
chain.add SidekiqScaling::Middleware
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
```
|
45
|
+
|
46
|
+
|
47
|
+
## Configuration
|
48
|
+
|
49
|
+
All configuration can be done in a `SidekiqAutoscale.configure` block, either in a standalone initializer or in `<environment>.rb` files.
|
50
|
+
|
51
|
+
Standard configuration looks like this:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
SidekiqAutoscale.configure do |config|
|
55
|
+
config.cache = Rails.cache
|
56
|
+
config.logger = Rails.logger
|
57
|
+
config.redis = # Put a real Redis client instance here
|
58
|
+
|
59
|
+
# Number of workers will never go below this threshold
|
60
|
+
config.min_workers = 1
|
61
|
+
|
62
|
+
# Number of workers will never go above this threshold
|
63
|
+
config.max_workers = 20
|
64
|
+
|
65
|
+
# The up and down thresholds used by all scaling strategies
|
66
|
+
config.scale_down_threshold = 1.0
|
67
|
+
config.scale_up_threshold = 5.0
|
68
|
+
|
69
|
+
# Current strategies are:
|
70
|
+
# :oldest_job - scales based on the age (in seconds) of the oldest job in any Sidekiq queue
|
71
|
+
# :delay_based - scales based on the average age (in seconds) all jobs run in the last minute
|
72
|
+
# :linear - scales based the total number of jobs in all queues, divided by the number of workers
|
73
|
+
# :base - do not scale, ever
|
74
|
+
config.strategy = :base
|
75
|
+
|
76
|
+
# Current adapters are:
|
77
|
+
# :nil - scaling events do nothing
|
78
|
+
# :heroku - scale a Heroku dyno
|
79
|
+
|
80
|
+
config.adapter = :heroku
|
81
|
+
|
82
|
+
# Any configuration required for the selected adapter
|
83
|
+
# Heroku requires the following:
|
84
|
+
config.adapter_config = {
|
85
|
+
api_key: "HEROKU_API_KEY",
|
86
|
+
worker_dyno_name: "DYNO_WORKER_NAME",
|
87
|
+
app_name: "HEROKU_APP_NAME"
|
88
|
+
}
|
89
|
+
# Kubernetes requires the following:
|
90
|
+
config.adapter_config = {
|
91
|
+
deployment_name: "myapp-sidekiq",
|
92
|
+
}
|
93
|
+
|
94
|
+
# The minimum amount of time to wait between scaling events
|
95
|
+
# Useful to tweak based on how long it takes for a new worker
|
96
|
+
# to spin up and start working on the pool
|
97
|
+
# config.min_scaling_interval = 5.minutes.to_i
|
98
|
+
|
99
|
+
# The number of workers to change in a scaling event
|
100
|
+
# config.scale_by = 1
|
101
|
+
|
102
|
+
# This proc will be called on a scaling event
|
103
|
+
# config.on_scaling_event = Proc.new { |event| Rails.logger.info event.to_json }
|
104
|
+
|
105
|
+
# This proc will be called when a scaling event errors out
|
106
|
+
# By default, nothing happens
|
107
|
+
# config.on_scaling_error = Proc.new { |error| Rails.logger.error error.to_json }
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
## Kubernetes
|
112
|
+
|
113
|
+
This gem can scale a Kubernetes `Deployment` object that Sidekiq is running in.
|
114
|
+
|
115
|
+
Doing so requires the following RBAC config:
|
116
|
+
|
117
|
+
```
|
118
|
+
|
119
|
+
---
|
120
|
+
kind: Role
|
121
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
122
|
+
metadata:
|
123
|
+
name: myapp-sidekiq
|
124
|
+
rules:
|
125
|
+
- apiGroups: ["apps"]
|
126
|
+
resources: ["deployments"]
|
127
|
+
verbs: ["get", "patch"]
|
128
|
+
|
129
|
+
---
|
130
|
+
kind: RoleBinding
|
131
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
132
|
+
metadata:
|
133
|
+
name: sidekiq
|
134
|
+
subjects:
|
135
|
+
- kind: ServiceAccount
|
136
|
+
name: myapp-sidekiq
|
137
|
+
roleRef:
|
138
|
+
kind: Role
|
139
|
+
name: myapp-sidekiq
|
140
|
+
apiGroup: rbac.authorization.k8s.io
|
141
|
+
|
142
|
+
---
|
143
|
+
kind: ServiceAccount
|
144
|
+
apiVersion: v1
|
145
|
+
metadata:
|
146
|
+
name: myapp-sidekiq
|
147
|
+
```
|
148
|
+
|
149
|
+
Then assign your sidekiq deployment the `myapp-sidekiq` service account.
|
150
|
+
|
151
|
+
## License
|
152
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "bundler/setup"
|
5
|
+
rescue LoadError
|
6
|
+
puts "You must `gem install bundler` and `bundle install` to run rake tasks"
|
7
|
+
end
|
8
|
+
|
9
|
+
require "rdoc/task"
|
10
|
+
|
11
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
12
|
+
rdoc.rdoc_dir = "rdoc"
|
13
|
+
rdoc.title = "SidekiqAutoscale"
|
14
|
+
rdoc.options << "--line-numbers"
|
15
|
+
rdoc.rdoc_files.include("README.md")
|
16
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
17
|
+
end
|
18
|
+
|
19
|
+
require "bundler/gem_tasks"
|
20
|
+
|
21
|
+
require "rake/testtask"
|
22
|
+
|
23
|
+
Rake::TestTask.new(:test) do |t|
|
24
|
+
t.libs << "test"
|
25
|
+
t.pattern = "test/**/*_test.rb"
|
26
|
+
t.verbose = false
|
27
|
+
end
|
28
|
+
|
29
|
+
task default: :test
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
|
5
|
+
module SidekiqAutoscale
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
8
|
+
|
9
|
+
def generate_install
|
10
|
+
template "sidekiq_autoscale_initializer_template.template", "config/initializers/sidekiq_autoscale.rb"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
SidekiqAutoscale.configure do |config|
|
4
|
+
config.cache = Rails.cache
|
5
|
+
config.logger = Rails.logger
|
6
|
+
config.redis_client = # Put a real Redis client instance here
|
7
|
+
|
8
|
+
# Number of workers will never go below this threshold
|
9
|
+
config.min_workers = 1
|
10
|
+
|
11
|
+
# Number of workers will never go above this threshold
|
12
|
+
config.max_workers = 20
|
13
|
+
|
14
|
+
# The up and down thresholds used by all scaling strategies
|
15
|
+
config.scale_down_threshold = 1.0
|
16
|
+
config.scale_up_threshold = 5.0
|
17
|
+
|
18
|
+
# Current strategies are:
|
19
|
+
# :oldest_job - scales based on the age (in seconds) of the oldest job in any Sidekiq queue
|
20
|
+
# :delay_based - scales based on the average age (in seconds) all jobs run in the last minute
|
21
|
+
# :linear - scales based the total number of jobs in all queues, divided by the number of workers
|
22
|
+
# :base - do not scale, ever
|
23
|
+
config.strategy = :base
|
24
|
+
|
25
|
+
# Current adapters are:
|
26
|
+
# :nil - scaling events do nothing
|
27
|
+
# :heroku - scale a Heroku dyno
|
28
|
+
|
29
|
+
config.adapter = :heroku
|
30
|
+
|
31
|
+
# Any configuration required for the selected adapter
|
32
|
+
# Heroku requires the following:
|
33
|
+
config.adapter_config = {
|
34
|
+
api_key: "HEROKU_API_KEY",
|
35
|
+
worker_dyno_name: "DYNO_WORKER_NAME",
|
36
|
+
app_name: "HEROKU_APP_NAME"
|
37
|
+
}
|
38
|
+
|
39
|
+
# The minimum amount of time to wait between scaling events
|
40
|
+
# Useful to tweak based on how long it takes for a new worker
|
41
|
+
# to spin up and start working on the pool
|
42
|
+
# config.min_scaling_interval = 5.minutes.to_i
|
43
|
+
|
44
|
+
# The number of workers to change in a scaling event
|
45
|
+
# config.scale_by = 1
|
46
|
+
|
47
|
+
# This proc will be called on a scaling event
|
48
|
+
# config.on_scaling_event = Proc.new { |event| Rails.logger.info event.to_json }
|
49
|
+
|
50
|
+
# This proc will be called when a scaling event errors out
|
51
|
+
# By default, nothing happens
|
52
|
+
# config.on_scaling_error = Proc.new { |error| Rails.logger.error error.to_json }
|
53
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SidekiqAutoscale
|
4
|
+
class HerokuAdapter
|
5
|
+
def initialize
|
6
|
+
require "platform-api"
|
7
|
+
@app_name = SidekiqAutoscale.adapter_config[:app_name]
|
8
|
+
@dyno_name = SidekiqAutoscale.adapter_config[:worker_dyno_name]
|
9
|
+
@client = PlatformAPI.connect_oauth(SidekiqAutoscale.adapter_config[:api_key])
|
10
|
+
end
|
11
|
+
|
12
|
+
def worker_count
|
13
|
+
@client.formation.list(@app_name)
|
14
|
+
.select {|i| i["type"] == @dyno_name }
|
15
|
+
.map {|i| i["quantity"] }
|
16
|
+
.reduce(0, &:+)
|
17
|
+
rescue Excon::Errors::Error => e
|
18
|
+
SidekiqAutoscale.on_scaling_error(e)
|
19
|
+
0
|
20
|
+
end
|
21
|
+
|
22
|
+
def worker_count=(val)
|
23
|
+
return if val == worker_count
|
24
|
+
|
25
|
+
SidekiqAutoscale.logger.info("[SIDEKIQ_SCALE][HEROKU_ACTION] Setting new worker count to #{val} (is currenly #{worker_count})")
|
26
|
+
@client.formation.update(@app_name, @dyno_name, quantity: val)
|
27
|
+
rescue Excon::Errors::Error, Heroku::API::Errors::Error => e
|
28
|
+
SidekiqAutoscale.on_scaling_error(e)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SidekiqAutoscale
|
4
|
+
# Scale a Kubernetes deployment object.
|
5
|
+
class KubernetesAdapter
|
6
|
+
def initialize
|
7
|
+
require "k8s-ruby"
|
8
|
+
|
9
|
+
namespace = File.read("/run/secrets/kubernetes.io/serviceaccount/namespace")
|
10
|
+
client = K8s::Client.autoconfig
|
11
|
+
|
12
|
+
@deployment_name = SidekiqAutoscale.adapter_config[:deployment_name]
|
13
|
+
@resources = client.api("apps/v1").resource("deployments", namespace: namespace)
|
14
|
+
end
|
15
|
+
|
16
|
+
def worker_count
|
17
|
+
@resources.get(@deployment_name).spec.replicas
|
18
|
+
rescue Excon::Errors::Error, K8s::Error, K8s::Error::Forbidden => e
|
19
|
+
SidekiqAutoscale.on_scaling_error(e)
|
20
|
+
0
|
21
|
+
end
|
22
|
+
|
23
|
+
def worker_count=(val)
|
24
|
+
return if val == worker_count
|
25
|
+
|
26
|
+
SidekiqAutoscale.logger.info("[SIDEKIQ_SCALE][KUBERNETES_ACTION] Setting new worker count to #{val} (is currenly #{worker_count})")
|
27
|
+
@resources.merge_patch(@deployment_name, spec: {replicas: val})
|
28
|
+
rescue Excon::Errors::Error, K8s::Error, K8s::Error::Forbidden => e
|
29
|
+
SidekiqAutoscale.on_scaling_error(e)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SidekiqAutoscale
|
4
|
+
class NilAdapter
|
5
|
+
def initialize
|
6
|
+
@sidekiq_adapter = ::SidekiqAutoscale::SidekiqInterface.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def worker_count
|
10
|
+
@sidekiq_adapter.total_workers
|
11
|
+
end
|
12
|
+
|
13
|
+
def worker_count=(val)
|
14
|
+
SidekiqAutoscale.logger.debug("Attempting to autoscale sidekiq to #{val} workers")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
File without changes
|
@@ -0,0 +1,201 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SidekiqAutoscale
|
4
|
+
module Config
|
5
|
+
module SharedConfigs
|
6
|
+
LOG_TAG = "[SIDEKIQ_SCALING]"
|
7
|
+
|
8
|
+
attr_writer :config
|
9
|
+
|
10
|
+
def config
|
11
|
+
@config ||= ActiveSupport::OrderedOptions.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def strategy
|
15
|
+
config.strategy || :base
|
16
|
+
end
|
17
|
+
|
18
|
+
def strategy_klass
|
19
|
+
@strategy_klass ||= begin
|
20
|
+
known_strats = [
|
21
|
+
::SidekiqAutoscale::Strategies::BaseScaling,
|
22
|
+
::SidekiqAutoscale::Strategies::DelayScaling,
|
23
|
+
::SidekiqAutoscale::Strategies::OldestJobScaling,
|
24
|
+
::SidekiqAutoscale::Strategies::LinearScaling,
|
25
|
+
::SidekiqAutoscale::Strategies::DynamicLatencyScaling
|
26
|
+
|
27
|
+
]
|
28
|
+
strat_klass_name = known_strats.map(&:to_s).find {|i| i.end_with?("#{strategy.to_s.camelize}Scaling") }
|
29
|
+
if strat_klass_name.nil?
|
30
|
+
raise ::SidekiqAutoscale::Exception.new <<~LOG
|
31
|
+
#{LOG_TAG} Unknown scaling strategy: [#{strategy.to_s.camelize}Scaling]")
|
32
|
+
LOG
|
33
|
+
end
|
34
|
+
|
35
|
+
strat_klass_name.constantize.new
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def adapter
|
40
|
+
config.adapter || :nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def adapter_klass
|
44
|
+
@adapter_klass ||= begin
|
45
|
+
known_adapters = [::SidekiqAutoscale::NilAdapter,
|
46
|
+
::SidekiqAutoscale::HerokuAdapter,
|
47
|
+
::SidekiqAutoscale::KubernetesAdapter].freeze
|
48
|
+
adapter_klass_name = known_adapters.map(&:to_s).find {|i| i.end_with?("#{adapter.to_s.camelize}Adapter") }
|
49
|
+
if adapter_klass_name.nil?
|
50
|
+
raise ::SidekiqAutoscale::Exception.new("#{LOG_TAG} Unknown scaling adapter: [#{adapter.to_s.camelize}Adapter]")
|
51
|
+
end
|
52
|
+
|
53
|
+
adapter_klass_name.constantize.new
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def adapter_config
|
58
|
+
config.adapter_config
|
59
|
+
end
|
60
|
+
|
61
|
+
def scale_up_threshold
|
62
|
+
(config.scale_up_threshold ||= begin
|
63
|
+
validate_scaling_thresholds
|
64
|
+
validated_scale_up_threshold
|
65
|
+
end).to_f
|
66
|
+
end
|
67
|
+
|
68
|
+
def scale_down_threshold
|
69
|
+
(config.scale_down_threshold ||= begin
|
70
|
+
validate_scaling_thresholds
|
71
|
+
validated_scale_down_threshold
|
72
|
+
end).to_f
|
73
|
+
end
|
74
|
+
|
75
|
+
def max_workers
|
76
|
+
(@max_workers ||= begin
|
77
|
+
validate_worker_set
|
78
|
+
validated_max_workers
|
79
|
+
end).to_i
|
80
|
+
end
|
81
|
+
|
82
|
+
def min_workers
|
83
|
+
(@min_workers ||= begin
|
84
|
+
validate_worker_set
|
85
|
+
validated_min_workers
|
86
|
+
end).to_i
|
87
|
+
end
|
88
|
+
|
89
|
+
def scale_by
|
90
|
+
(config.scale_by || ENV.fetch("SIDEKIQ_AUTOSCALE_SCALE_BY", 1)).to_i
|
91
|
+
end
|
92
|
+
|
93
|
+
def min_scaling_interval
|
94
|
+
(config.min_scaling_interval || 5.minutes).to_i
|
95
|
+
end
|
96
|
+
|
97
|
+
def redis_client
|
98
|
+
raise ::SidekiqAutoscale::Exception.new("No Redis client defined") unless config.redis_client
|
99
|
+
|
100
|
+
config.redis_client
|
101
|
+
end
|
102
|
+
|
103
|
+
def logger
|
104
|
+
config.logger ||= Rails.logger
|
105
|
+
end
|
106
|
+
|
107
|
+
def cache
|
108
|
+
config.cache ||= ActiveSupport::Cache::NullStore.new
|
109
|
+
end
|
110
|
+
|
111
|
+
def on_scaling_error(e)
|
112
|
+
logger.error(e)
|
113
|
+
return unless config.on_scaling_error.respond_to?(:call)
|
114
|
+
|
115
|
+
config.on_scaling_error.call(e)
|
116
|
+
end
|
117
|
+
|
118
|
+
def on_scaling_event(event)
|
119
|
+
details = config.to_h.slice(:strategy,
|
120
|
+
:adapter,
|
121
|
+
:scale_up_threshold,
|
122
|
+
:scale_down_threshold,
|
123
|
+
:max_workers,
|
124
|
+
:min_workers,
|
125
|
+
:scale_by,
|
126
|
+
:min_scaling_interval)
|
127
|
+
|
128
|
+
on_head_bump(details.merge(event)) if event[:target_workers] == max_workers
|
129
|
+
on_toe_stub(details.merge(event)) if event[:target_workers] == min_workers
|
130
|
+
|
131
|
+
return unless config.on_scaling_event.respond_to?(:call)
|
132
|
+
|
133
|
+
config.on_scaling_event.call(details.merge(event))
|
134
|
+
end
|
135
|
+
|
136
|
+
def on_head_bump(event)
|
137
|
+
return unless config.on_head_bump.respond_to?(:call)
|
138
|
+
|
139
|
+
config.on_head_bump.call(event)
|
140
|
+
end
|
141
|
+
|
142
|
+
def on_toe_stub(event)
|
143
|
+
return unless config.on_toe_stub.respond_to?(:call)
|
144
|
+
|
145
|
+
config.on_toe_stub.call(event)
|
146
|
+
end
|
147
|
+
|
148
|
+
def sidekiq_interface
|
149
|
+
@sidekiq_interface ||= ::SidekiqAutoscale::SidekiqInterface.new
|
150
|
+
end
|
151
|
+
|
152
|
+
def lock_manager
|
153
|
+
config.lock_manager ||= ::Redlock::Client.new(Array.wrap(redis_client),
|
154
|
+
retry_count: 3,
|
155
|
+
retry_delay: 200,
|
156
|
+
retry_jitter: 50,
|
157
|
+
redis_timeout: 0.1)
|
158
|
+
end
|
159
|
+
|
160
|
+
def lock_time
|
161
|
+
config.lock_time || 5_000
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
def validate_worker_set
|
167
|
+
ex_klass = ::SidekiqAutoscale::Exception
|
168
|
+
raise ex_klass.new("No max workers set") unless validated_max_workers.positive?
|
169
|
+
raise ex_klass.new("No min workers set") unless validated_min_workers.positive?
|
170
|
+
if validated_max_workers < validated_min_workers
|
171
|
+
raise ex_klass.new("Max workers must be higher than min workers")
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def validate_scaling_thresholds
|
176
|
+
ex_klass = ::SidekiqAutoscale::Exception
|
177
|
+
raise ex_klass.new("No scale up threshold set") unless validated_scale_up_threshold.positive?
|
178
|
+
raise ex_klass.new("No scale down threshold set") unless validated_scale_down_threshold.positive?
|
179
|
+
if validated_scale_up_threshold < validated_scale_down_threshold
|
180
|
+
raise ex_klass.new("Scale up threshold must be higher than scale down threshold")
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def validated_scale_up_threshold
|
185
|
+
(config.scale_up_threshold || ENV.fetch("SIDEKIQ_AUTOSCALE_UP_THRESHOLD", 5.0)).to_f
|
186
|
+
end
|
187
|
+
|
188
|
+
def validated_scale_down_threshold
|
189
|
+
(config.scale_down_threshold || ENV.fetch("SIDEKIQ_AUTOSCALE_DOWN_THRESHOLD", 1.0)).to_f
|
190
|
+
end
|
191
|
+
|
192
|
+
def validated_max_workers
|
193
|
+
(config.max_workers || ENV.fetch("SIDEKIQ_AUTOSCALE_MAX_WORKERS", 10)).to_i
|
194
|
+
end
|
195
|
+
|
196
|
+
def validated_min_workers
|
197
|
+
(config.min_workers || ENV.fetch("SIDEKIQ_AUTOSCALE_MIN_WORKERS", 1)).to_i
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SidekiqAutoscale
|
4
|
+
class Middleware
|
5
|
+
LAST_SCALED_AT_EVENT_KEY = "sidekiq_autoscaling:last_scaled_at"
|
6
|
+
SCALING_LOCK_KEY = "sidekiq_autoscaling:scaling_lock"
|
7
|
+
LOG_TAG = "[SIDEKIQ_SCALE][SCALING_EVENT]"
|
8
|
+
WORKER_COUNT_KEY = "sidekiq_autoscaling/current_worker_count"
|
9
|
+
|
10
|
+
# @param [Object] worker the worker instance
|
11
|
+
# @param [Hash] job the full job payload
|
12
|
+
# * @see https://github.com/mperham/sidekiq/wiki/Job-Format
|
13
|
+
# @param [String] queue the name of the queue the job was pulled from
|
14
|
+
# @yield the next middleware in the chain or worker `perform` method
|
15
|
+
# @return [Void]
|
16
|
+
def call(_worker_class, job, _queue)
|
17
|
+
# In case the scaling strategy needs to record job-specific stuff before it runs
|
18
|
+
SidekiqAutoscale.strategy_klass.log_job(job)
|
19
|
+
yield # Run the job, THEN scale the cluster
|
20
|
+
begin
|
21
|
+
return unless SidekiqAutoscale.strategy_klass.workload_change_needed?(job)
|
22
|
+
|
23
|
+
direction = SidekiqAutoscale.strategy_klass.scaling_direction(job)
|
24
|
+
new_worker_count = worker_count + (SidekiqAutoscale.scale_by * direction)
|
25
|
+
|
26
|
+
set_worker_count(new_worker_count, event_id: job["jid"], direction: direction)
|
27
|
+
rescue StandardError => e
|
28
|
+
SidekiqAutoscale.logger.error(e)
|
29
|
+
SidekiqAutoscale.on_scaling_error(e)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def worker_count
|
36
|
+
SidekiqAutoscale.cache.fetch(WORKER_COUNT_KEY, expires_in: 1.minute) do
|
37
|
+
SidekiqAutoscale.adapter_klass.worker_count
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_worker_count(n, event_id: SecureRandom.hex, direction:)
|
42
|
+
clamped = n.clamp(SidekiqAutoscale.min_workers, SidekiqAutoscale.max_workers)
|
43
|
+
|
44
|
+
SidekiqAutoscale.lock_manager.lock(SCALING_LOCK_KEY, SidekiqAutoscale.lock_time) do |locked|
|
45
|
+
# Not awesome, but gotta handle the initial nil case
|
46
|
+
last_scaled_at = SidekiqAutoscale.redis_client.get(LAST_SCALED_AT_EVENT_KEY).to_f
|
47
|
+
SidekiqAutoscale.logger.debug <<~LOG
|
48
|
+
#{LOG_TAG}[#{event_id}] Concurrency lock obtained: #{locked}"
|
49
|
+
Last scaled [#{Time.current.to_i - last_scaled_at.to_i}] seconds ago"
|
50
|
+
Scaling every [#{SidekiqAutoscale.min_scaling_interval}] seconds"
|
51
|
+
LOG
|
52
|
+
|
53
|
+
if locked && (last_scaled_at < SidekiqAutoscale.min_scaling_interval.seconds.ago.to_f)
|
54
|
+
SidekiqAutoscale.adapter_klass.worker_count = clamped
|
55
|
+
SidekiqAutoscale.cache.delete(WORKER_COUNT_KEY)
|
56
|
+
SidekiqAutoscale.redis_client.set(LAST_SCALED_AT_EVENT_KEY, Time.current.to_f)
|
57
|
+
SidekiqAutoscale.on_scaling_event(
|
58
|
+
direction: direction,
|
59
|
+
target_workers: clamped,
|
60
|
+
event_id: event_id,
|
61
|
+
current_worker_count: worker_count,
|
62
|
+
last_scaled_at: last_scaled_at
|
63
|
+
)
|
64
|
+
else
|
65
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG}[#{event_id}] ***NOT SCALING***")
|
66
|
+
end
|
67
|
+
|
68
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG}[#{event_id}] RELEASING LOCK #{locked}") if locked
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails"
|
4
|
+
|
5
|
+
module SidekiqAutoscale
|
6
|
+
class Railtie < ::Rails::Railtie
|
7
|
+
config.sidekiq_autoscale = ActiveSupport::OrderedOptions.new
|
8
|
+
|
9
|
+
config.after_initialize do
|
10
|
+
SidekiqAutoscale.logger.info <<~LOG
|
11
|
+
[SIDEKIQ_SCALE] Scaling strategy: #{SidekiqAutoscale.strategy}
|
12
|
+
[SIDEKIQ_SCALE] Min workers: #{SidekiqAutoscale.min_workers}
|
13
|
+
[SIDEKIQ_SCALE] Max workers: #{SidekiqAutoscale.max_workers}
|
14
|
+
[SIDEKIQ_SCALE] Scaling by: #{SidekiqAutoscale.scale_by}
|
15
|
+
[SIDEKIQ_SCALE] Provider adapter: #{SidekiqAutoscale.adapter}
|
16
|
+
LOG
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sidekiq/api"
|
4
|
+
|
5
|
+
module SidekiqAutoscale
|
6
|
+
class SidekiqInterface
|
7
|
+
def total_queue_size
|
8
|
+
queue_names.map {|q| ::Sidekiq::Queue.new(q).size }.reduce(0, &:+)
|
9
|
+
end
|
10
|
+
|
11
|
+
def queue_names
|
12
|
+
::Sidekiq::Queue.all.map(&:name)
|
13
|
+
end
|
14
|
+
|
15
|
+
def busy_threads
|
16
|
+
::Sidekiq::Workers.new.map {|_, thread, _| thread }.uniq.size
|
17
|
+
end
|
18
|
+
|
19
|
+
def latency
|
20
|
+
queue_names.map {|q| ::Sidekiq::Queue.new(q).latency }.max
|
21
|
+
end
|
22
|
+
|
23
|
+
def total_workers
|
24
|
+
process_set.size
|
25
|
+
end
|
26
|
+
|
27
|
+
def total_threads
|
28
|
+
process_set.map {|w| w["concurrency"] }.reduce(0, &:+)
|
29
|
+
end
|
30
|
+
|
31
|
+
def available_threads
|
32
|
+
total_threads - busy_threads
|
33
|
+
end
|
34
|
+
|
35
|
+
def youngest_worker
|
36
|
+
process_set.map {|w| w["started_at"] }.max
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def process_set
|
42
|
+
@process_set ||= ::Sidekiq::ProcessSet.new
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SidekiqAutoscale
|
4
|
+
module Strategies
|
5
|
+
class BaseScaling
|
6
|
+
# This strategy doesn't care about individual job metrics
|
7
|
+
def log_job(_job); end
|
8
|
+
|
9
|
+
def workload_change_needed?(_job)
|
10
|
+
false
|
11
|
+
end
|
12
|
+
|
13
|
+
def scaling_direction(_job)
|
14
|
+
0
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def scale_up_threshold
|
20
|
+
SidekiqAutoscale.scale_up_threshold
|
21
|
+
end
|
22
|
+
|
23
|
+
def scale_down_threshold
|
24
|
+
SidekiqAutoscale.scale_down_threshold
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SidekiqAutoscale
|
4
|
+
module Strategies
|
5
|
+
class DelayScaling < BaseScaling
|
6
|
+
SAMPLE_RANGE = 60
|
7
|
+
DELAY_LOG_KEY = "sidekiq_autoscaling:delay_log"
|
8
|
+
DELAY_AVERAGE_CACHE_KEY = "sidekiq_autoscaling:delay_average"
|
9
|
+
LOG_TAG = "[SIDEKIQ_SCALE][DELAY_SCALING]"
|
10
|
+
|
11
|
+
def log_job(job)
|
12
|
+
timestamp = Time.current.to_f
|
13
|
+
|
14
|
+
# Gotta do it this way so that each entry is guaranteed to be unique
|
15
|
+
zset_payload = {delay: (timestamp - job["enqueued_at"]), jid: job["jid"]}.to_json
|
16
|
+
|
17
|
+
# Redis zadd runs in O(log(N)) time, so this should be threaded to avoid blocking
|
18
|
+
# Also, it should be connection-pooled, but I can't remember if we're using
|
19
|
+
# redis connection pooling anywhere
|
20
|
+
Thread.new {
|
21
|
+
SidekiqAutoscale.redis_client.zadd(DELAY_LOG_KEY, timestamp, zset_payload)
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def workload_change_needed?(_job)
|
26
|
+
workload_too_high? || workload_too_low?
|
27
|
+
end
|
28
|
+
|
29
|
+
def scaling_direction(_job)
|
30
|
+
return -1 if workload_too_low?
|
31
|
+
return 1 if workload_too_high?
|
32
|
+
|
33
|
+
0
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def delay_average
|
39
|
+
# Only calculate this once every minute - this operation isn't very efficient
|
40
|
+
# We may want to offload it to another Redis DB number, which will be just delay keys
|
41
|
+
SidekiqAutoscale.cache.fetch(DELAY_AVERAGE_CACHE_KEY, expires_in: SAMPLE_RANGE) do
|
42
|
+
# Delete old scores that won't be included in the metric
|
43
|
+
SidekiqAutoscale.redis_client.zremrangebyscore(DELAY_LOG_KEY, 0, SAMPLE_RANGE.ago.to_f)
|
44
|
+
vals = SidekiqAutoscale.redis_client.zrange(DELAY_LOG_KEY, 0, -1).map {|i| JSON.parse(i)["delay"].to_f }
|
45
|
+
return 0 if vals.empty?
|
46
|
+
|
47
|
+
vals.instance_eval { reduce(:+) / size.to_f }
|
48
|
+
rescue JSON::ParserError => e
|
49
|
+
SidekiqAutoscale.logger.error(e)
|
50
|
+
SidekiqAutoscale.logger.error(e.backtrace.join("\n"))
|
51
|
+
return 0
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def workload_too_high?
|
56
|
+
too_high = delay_average > SidekiqAutoscale.scale_up_threshold
|
57
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too high") if too_high
|
58
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG} Current average delay: #{delay_average}, max allowed: #{SidekiqAutoscale.scale_up_threshold}")
|
59
|
+
too_high
|
60
|
+
end
|
61
|
+
|
62
|
+
def workload_too_low?
|
63
|
+
too_low = delay_average < SidekiqAutoscale.scale_down_threshold
|
64
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too low") if too_low
|
65
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG} Current average delay: #{delay_average}, min allowed: #{SidekiqAutoscale.scale_down_threshold}")
|
66
|
+
too_low
|
67
|
+
end
|
68
|
+
|
69
|
+
def delay_array; end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SidekiqAutoscale
|
4
|
+
module Strategies
|
5
|
+
class DynamicLatencyScaling < BaseScaling
|
6
|
+
LOG_TAG = "[SIDEKIQ_SCALE][DYNAMIC_LATENCY_SCALING]"
|
7
|
+
def workload_change_needed?(_job)
|
8
|
+
workload_too_high? || workload_too_low?
|
9
|
+
end
|
10
|
+
|
11
|
+
def scaling_direction(_job)
|
12
|
+
return -1 if workload_too_low?
|
13
|
+
return [scale_up_factor.to_i, 1].max if workload_too_high?
|
14
|
+
|
15
|
+
0
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def scale_up_factor
|
21
|
+
1 + (latency - scale_up_threshold) / dynamic_multiple_base
|
22
|
+
end
|
23
|
+
|
24
|
+
def dynamic_multiple_base
|
25
|
+
@dynamic_multiple_base ||= scale_up_threshold - scale_down_threshold
|
26
|
+
end
|
27
|
+
|
28
|
+
def workload_too_high?
|
29
|
+
too_high = latency > scale_up_threshold
|
30
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too high") if too_high
|
31
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG} Current average delay: #{latency}, max allowed: #{scale_up_threshold}")
|
32
|
+
too_high
|
33
|
+
end
|
34
|
+
|
35
|
+
def workload_too_low?
|
36
|
+
too_low = latency < scale_down_threshold
|
37
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too low") if too_low
|
38
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG} Current average delay: #{latency}, min allowed: #{scale_down_threshold}")
|
39
|
+
too_low
|
40
|
+
end
|
41
|
+
|
42
|
+
def latency
|
43
|
+
SidekiqAutoscale.sidekiq_interface.latency
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SidekiqAutoscale
|
4
|
+
module Strategies
|
5
|
+
class LinearScaling < BaseScaling
|
6
|
+
LOG_TAG = "[SIDEKIQ_SCALE][LINEAR_SCALING]"
|
7
|
+
|
8
|
+
def workload_change_needed?(_job)
|
9
|
+
workload_too_high? || workload_too_low?
|
10
|
+
end
|
11
|
+
|
12
|
+
def scaling_direction(_job)
|
13
|
+
return 1 if workload_too_high?
|
14
|
+
return -1 if workload_too_low?
|
15
|
+
|
16
|
+
0
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Remove available threads from total queue size in case there's pending
|
22
|
+
# tasks that are still spinning up,
|
23
|
+
def scheduled_jobs_per_thread
|
24
|
+
((SidekiqAutoscale.sidekiq_interface.total_queue_size - SidekiqAutoscale.sidekiq_interface.available_threads).to_f / SidekiqAutoscale.sidekiq_interface.total_threads.to_f)
|
25
|
+
end
|
26
|
+
|
27
|
+
def workload_too_high?
|
28
|
+
too_high = scheduled_jobs_per_thread > SidekiqAutoscale.scale_up_threshold
|
29
|
+
if too_high
|
30
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too low [Scheduled: #{scheduled_jobs_per_thread}, Max: #{SidekiqAutoscale.scale_up_threshold}]")
|
31
|
+
end
|
32
|
+
too_high
|
33
|
+
end
|
34
|
+
|
35
|
+
def workload_too_low?
|
36
|
+
too_low = scheduled_jobs_per_thread < SidekiqAutoscale.scale_down_threshold
|
37
|
+
if too_low
|
38
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too low [Scheduled: #{scheduled_jobs_per_thread}, Min: #{SidekiqAutoscale.scale_down_threshold}]")
|
39
|
+
end
|
40
|
+
too_low
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SidekiqAutoscale
|
4
|
+
module Strategies
|
5
|
+
class OldestJobScaling < BaseScaling
|
6
|
+
LOG_TAG = "[SIDEKIQ_SCALE][OLDEST_JOB_SCALING]"
|
7
|
+
def workload_change_needed?(_job)
|
8
|
+
workload_too_high? || workload_too_low?
|
9
|
+
end
|
10
|
+
|
11
|
+
def scaling_direction(_job)
|
12
|
+
return -1 if workload_too_low?
|
13
|
+
return 1 if workload_too_high?
|
14
|
+
|
15
|
+
0
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def workload_too_high?
|
21
|
+
too_high = SidekiqAutoscale.sidekiq_interface.latency > SidekiqAutoscale.scale_up_threshold
|
22
|
+
|
23
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too high") if too_high
|
24
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG} Current average delay: #{SidekiqAutoscale.sidekiq_interface.latency}, max allowed: #{SidekiqAutoscale.scale_up_threshold}")
|
25
|
+
too_high
|
26
|
+
end
|
27
|
+
|
28
|
+
def workload_too_low?
|
29
|
+
too_low = SidekiqAutoscale.sidekiq_interface.latency < SidekiqAutoscale.scale_down_threshold
|
30
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG} Workload too low") if too_low
|
31
|
+
SidekiqAutoscale.logger.debug("#{LOG_TAG} Current average delay: #{SidekiqAutoscale.sidekiq_interface.latency}, min allowed: #{SidekiqAutoscale.scale_down_threshold}")
|
32
|
+
too_low
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redlock"
|
4
|
+
require "sidekiq/api"
|
5
|
+
require "active_support/all"
|
6
|
+
|
7
|
+
require "sidekiq_autoscale/railtie"
|
8
|
+
require "sidekiq_autoscale/exception"
|
9
|
+
require "sidekiq_autoscale/sidekiq_interface"
|
10
|
+
require "sidekiq_autoscale/strategies/base_scaling"
|
11
|
+
require "sidekiq_autoscale/strategies/delay_scaling"
|
12
|
+
require "sidekiq_autoscale/strategies/linear_scaling"
|
13
|
+
require "sidekiq_autoscale/strategies/oldest_job_scaling"
|
14
|
+
require "sidekiq_autoscale/strategies/dynamic_latency_scaling"
|
15
|
+
require "sidekiq_autoscale/adapters/nil_adapter"
|
16
|
+
require "sidekiq_autoscale/adapters/heroku_adapter"
|
17
|
+
require "sidekiq_autoscale/adapters/kubernetes_adapter"
|
18
|
+
require "sidekiq_autoscale/middleware"
|
19
|
+
require "sidekiq_autoscale/config/callbacks"
|
20
|
+
require "sidekiq_autoscale/config/shared_configs"
|
21
|
+
|
22
|
+
module SidekiqAutoscale
|
23
|
+
class << self
|
24
|
+
include SidekiqAutoscale::Config::SharedConfigs
|
25
|
+
|
26
|
+
def configure
|
27
|
+
yield config
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,365 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sidekiq_autoscale
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Steven Allen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-03-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: addressable
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.8'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: k8s-ruby
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.12'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.12'
|
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: '2.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: railties
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '6'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '6'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: redlock
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sidekiq
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '6'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '6'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: thor
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.19'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.19'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: awesome_print
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: bundler
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '2'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '2'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: byebug
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: guard
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '2'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '2'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: guard-bundler
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '2'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '2'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: guard-rspec
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - "~>"
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '4.7'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - "~>"
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '4.7'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: mock_redis
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0'
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: rspec
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - ">="
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '3.2'
|
216
|
+
- - "<"
|
217
|
+
- !ruby/object:Gem::Version
|
218
|
+
version: '4'
|
219
|
+
type: :development
|
220
|
+
prerelease: false
|
221
|
+
version_requirements: !ruby/object:Gem::Requirement
|
222
|
+
requirements:
|
223
|
+
- - ">="
|
224
|
+
- !ruby/object:Gem::Version
|
225
|
+
version: '3.2'
|
226
|
+
- - "<"
|
227
|
+
- !ruby/object:Gem::Version
|
228
|
+
version: '4'
|
229
|
+
- !ruby/object:Gem::Dependency
|
230
|
+
name: rubocop
|
231
|
+
requirement: !ruby/object:Gem::Requirement
|
232
|
+
requirements:
|
233
|
+
- - ">="
|
234
|
+
- !ruby/object:Gem::Version
|
235
|
+
version: '0.50'
|
236
|
+
type: :development
|
237
|
+
prerelease: false
|
238
|
+
version_requirements: !ruby/object:Gem::Requirement
|
239
|
+
requirements:
|
240
|
+
- - ">="
|
241
|
+
- !ruby/object:Gem::Version
|
242
|
+
version: '0.50'
|
243
|
+
- !ruby/object:Gem::Dependency
|
244
|
+
name: rubocop-rspec
|
245
|
+
requirement: !ruby/object:Gem::Requirement
|
246
|
+
requirements:
|
247
|
+
- - "~>"
|
248
|
+
- !ruby/object:Gem::Version
|
249
|
+
version: '1'
|
250
|
+
type: :development
|
251
|
+
prerelease: false
|
252
|
+
version_requirements: !ruby/object:Gem::Requirement
|
253
|
+
requirements:
|
254
|
+
- - "~>"
|
255
|
+
- !ruby/object:Gem::Version
|
256
|
+
version: '1'
|
257
|
+
- !ruby/object:Gem::Dependency
|
258
|
+
name: simplecov
|
259
|
+
requirement: !ruby/object:Gem::Requirement
|
260
|
+
requirements:
|
261
|
+
- - "~>"
|
262
|
+
- !ruby/object:Gem::Version
|
263
|
+
version: '0.16'
|
264
|
+
type: :development
|
265
|
+
prerelease: false
|
266
|
+
version_requirements: !ruby/object:Gem::Requirement
|
267
|
+
requirements:
|
268
|
+
- - "~>"
|
269
|
+
- !ruby/object:Gem::Version
|
270
|
+
version: '0.16'
|
271
|
+
- !ruby/object:Gem::Dependency
|
272
|
+
name: simplecov-console
|
273
|
+
requirement: !ruby/object:Gem::Requirement
|
274
|
+
requirements:
|
275
|
+
- - "~>"
|
276
|
+
- !ruby/object:Gem::Version
|
277
|
+
version: '0.4'
|
278
|
+
type: :development
|
279
|
+
prerelease: false
|
280
|
+
version_requirements: !ruby/object:Gem::Requirement
|
281
|
+
requirements:
|
282
|
+
- - "~>"
|
283
|
+
- !ruby/object:Gem::Version
|
284
|
+
version: '0.4'
|
285
|
+
- !ruby/object:Gem::Dependency
|
286
|
+
name: sqlite3
|
287
|
+
requirement: !ruby/object:Gem::Requirement
|
288
|
+
requirements:
|
289
|
+
- - ">="
|
290
|
+
- !ruby/object:Gem::Version
|
291
|
+
version: '0'
|
292
|
+
type: :development
|
293
|
+
prerelease: false
|
294
|
+
version_requirements: !ruby/object:Gem::Requirement
|
295
|
+
requirements:
|
296
|
+
- - ">="
|
297
|
+
- !ruby/object:Gem::Version
|
298
|
+
version: '0'
|
299
|
+
- !ruby/object:Gem::Dependency
|
300
|
+
name: webmock
|
301
|
+
requirement: !ruby/object:Gem::Requirement
|
302
|
+
requirements:
|
303
|
+
- - ">="
|
304
|
+
- !ruby/object:Gem::Version
|
305
|
+
version: '0'
|
306
|
+
type: :development
|
307
|
+
prerelease: false
|
308
|
+
version_requirements: !ruby/object:Gem::Requirement
|
309
|
+
requirements:
|
310
|
+
- - ">="
|
311
|
+
- !ruby/object:Gem::Version
|
312
|
+
version: '0'
|
313
|
+
description: A simple gem to handle Sidekiq autoscaling.
|
314
|
+
email:
|
315
|
+
- sallen@tractionguest.com
|
316
|
+
executables: []
|
317
|
+
extensions: []
|
318
|
+
extra_rdoc_files: []
|
319
|
+
files:
|
320
|
+
- MIT-LICENSE
|
321
|
+
- README.md
|
322
|
+
- Rakefile
|
323
|
+
- lib/generators/sidekiq_autoscale/install/install_generator.rb
|
324
|
+
- lib/generators/sidekiq_autoscale/install/templates/sidekiq_autoscale_initializer_template.template
|
325
|
+
- lib/sidekiq_autoscale.rb
|
326
|
+
- lib/sidekiq_autoscale/adapters/heroku_adapter.rb
|
327
|
+
- lib/sidekiq_autoscale/adapters/kubernetes_adapter.rb
|
328
|
+
- lib/sidekiq_autoscale/adapters/nil_adapter.rb
|
329
|
+
- lib/sidekiq_autoscale/config/callbacks.rb
|
330
|
+
- lib/sidekiq_autoscale/config/shared_configs.rb
|
331
|
+
- lib/sidekiq_autoscale/exception.rb
|
332
|
+
- lib/sidekiq_autoscale/middleware.rb
|
333
|
+
- lib/sidekiq_autoscale/railtie.rb
|
334
|
+
- lib/sidekiq_autoscale/sidekiq_interface.rb
|
335
|
+
- lib/sidekiq_autoscale/strategies/base_scaling.rb
|
336
|
+
- lib/sidekiq_autoscale/strategies/delay_scaling.rb
|
337
|
+
- lib/sidekiq_autoscale/strategies/dynamic_latency_scaling.rb
|
338
|
+
- lib/sidekiq_autoscale/strategies/linear_scaling.rb
|
339
|
+
- lib/sidekiq_autoscale/strategies/oldest_job_scaling.rb
|
340
|
+
- lib/sidekiq_autoscale/version.rb
|
341
|
+
- lib/tasks/sidekiq_autoscale_tasks.rake
|
342
|
+
homepage: https://github.com/tractionguest/sidekiq_autoscaling
|
343
|
+
licenses:
|
344
|
+
- MIT
|
345
|
+
metadata: {}
|
346
|
+
post_install_message:
|
347
|
+
rdoc_options: []
|
348
|
+
require_paths:
|
349
|
+
- lib
|
350
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
351
|
+
requirements:
|
352
|
+
- - ">="
|
353
|
+
- !ruby/object:Gem::Version
|
354
|
+
version: '0'
|
355
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
356
|
+
requirements:
|
357
|
+
- - ">="
|
358
|
+
- !ruby/object:Gem::Version
|
359
|
+
version: '0'
|
360
|
+
requirements: []
|
361
|
+
rubygems_version: 3.2.32
|
362
|
+
signing_key:
|
363
|
+
specification_version: 4
|
364
|
+
summary: A simple gem to handle Sidekiq autoscaling.
|
365
|
+
test_files: []
|