redis_cloud_auto_upgrade 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b0af044eab29e571790dc677565d64bd0a19c134
4
+ data.tar.gz: c015cdc4d6a083ca6c2a1083952e54874ed22a91
5
+ SHA512:
6
+ metadata.gz: bd1f6049bda02b71a68c38e45ca462661673948a2fd9f49dc140dc71c97e177d0e4261851c2e88161c9c9b9808f05ebf031a8d501e5d1b802168db9aa8f98a08
7
+ data.tar.gz: 94ddb4fd1c011450ebc3ee8960a4d7090b90067cfc49a04930fa676b1c6f7c87c85209c4177edf7e487d326634a0fd798a19ede46c8f5dccfa8e77f82f9b5c36
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Robert Dober, Guillaume Leseur
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # redis_cloud_auto_upgrade
2
+
3
+ ## Summary
4
+
5
+ A gem to allow to automatically upgrade your Redis Cloud Addon on Heroku (via Heroku's platform API)
6
+ in case a certain memory usage treshold is reached.
7
+
8
+ The following can be configured
9
+
10
+ - Heroku app and addon
11
+ - Heroku's API key
12
+ - Treshold
13
+
14
+ Typically it will be used in a background job.
15
+
16
+ ## Gotchas
17
+
18
+ * RedisCloud plan data is hardcoded into the gem for now.
19
+
20
+ * Only the current Redis instance is used to determine the used memory.
21
+
22
+ ## Usage
23
+
24
+ ### Official API
25
+
26
+ The following is the defined API as protected by semantic version and being **threadsafe**.
27
+
28
+ ```ruby
29
+ require 'redis_cloud_auto_upgrade'
30
+
31
+ # E.g. in a Sidekiq Worker
32
+
33
+ def perform
34
+ RedisCloudAutoUpgrade.potential_upgrade!(
35
+ # API key for the Heroku API required
36
+ heroku_api_key: "a88a8aa8-a8a8-4b57-a8aa-8888aa8888aa",
37
+
38
+ # Id of addon, which must be a rediscloud addon_service required
39
+ redis_cloud_id: = "4ceaf719-8a4b-4b8b-8dcf-7852fa79ec44"
40
+
41
+ # Upgrade as soon as 50% of the available memory is used defaults to 0.5
42
+ treshhold: 0.5
43
+
44
+ # Logging is provided if enabled as follows optional
45
+ logger: Rails.logger
46
+ ) do | upgrade_info |
47
+ # Callback in case an upgrade is actually done optional
48
+ # upgrade_info is a Hash see below for more information
49
+ ...
50
+ end
51
+ end
52
+ ```
53
+
54
+
55
+ Its return value is false if no upgrade was applied, true otherwise.
56
+
57
+ However to get more information you can provide a block which will be called with an `OpenStruct` instance, containing the following values
58
+
59
+ ```ruby
60
+ {
61
+ updated_at: DateTime,
62
+ old_plan: "rediscloud:100",
63
+ new_plan: "rediscloud:200",
64
+ mem_usage: 12_345_678, # in bytes
65
+ mem_usage_in_percent: 65, # Mem usage that triggered the upgrade
66
+ treshhold_in_percent: 50 # Mem usage >= treshhold triggers upgrades
67
+ }
68
+ ```
69
+
70
+ ### Inofficial API
71
+
72
+ Not protected by semantic versioning and **not threadsafe** is the API of the underlying object.
73
+
74
+ It is however very useful for experimenting:
75
+
76
+ ```ruby
77
+
78
+
79
+ rcau = RedisCloudAutoUpgrade.new
80
+ rcau.configure(heroku_api_key: 'aaaaaaaa...', heroku_app_name: 'myapp').configure(treshhold: 0.2) # 20%
81
+
82
+ rcau.needs_to_upgrade? #-> true
83
+ rcau.configure(treshhold: 0.8)
84
+ rcau.needs_to_upgrade? #-> false
85
+
86
+ rcau.configure(logger: Logger.new($stdout))
87
+
88
+ rcau.current_redis_cloud_plan #-> rediscloud:100
89
+ # Memoized value create a new object if interested in changing values
90
+
91
+ rcau.current_redis_mem_usage #-> 200300400 bytes, Memoized too
92
+ ```
93
+
94
+ ## Developer's Guide
95
+
96
+ ### Testing
97
+
98
+ We are testing with VCR and in order to setup the cassettes we upgrade a plan to `rediscloud:100` causing some cost.
99
+
100
+ If you want to test here is how to do it:
101
+
102
+ N.B. **Any time you test without the VCR cassettes you will spend some cents on the RedisCloud plan upgrade**
103
+
104
+ * Setup the two env variables `$HEROKU_APP_NAME` and `$HEROKU_API_KEY` correctly.
105
+
106
+ * Run the tests (this will fill `fixtures/vcr_cassettes` in your local copy, these files are not versioned as they contain your API KEY.)
107
+
108
+ * As you have just upgraded your Redis Cloud Plan to `rediscloud:100` do downgrade it to minimize the costs
109
+
110
+ ## Dependencies
111
+
112
+ * [Redis](https://github.com/redis/redis-rb)
113
+
114
+ * [PlatformAPI](https://github.com/heroku/platform-api)
115
+
116
+ ## References
117
+
118
+ * [Heroku Platform API / Addons](https://devcenter.heroku.com/articles/platform-api-reference#add-on)
119
+
120
+ ## Copyright
121
+
122
+ MIT, c.f. [LICENSE](LICENSE)
123
+
124
+ ## Authors
125
+
126
+ Guillaume Leseur
127
+ Robert Dober
@@ -0,0 +1,51 @@
1
+ # Configuration part
2
+ class RedisCloudAutoUpgrade
3
+ Configuration = Struct.new(
4
+ :heroku_api_key,
5
+ :heroku_app_name,
6
+ :logger,
7
+ :on_upgrade,
8
+ :redis_instance,
9
+ :treshhold
10
+ ) do
11
+ attr_reader :errors
12
+
13
+ def configure(**values)
14
+ values.each do |key, val|
15
+ self[key] = val
16
+ end
17
+ self
18
+ end
19
+
20
+ def only(*keys)
21
+ keys.inject({})do |h, k|
22
+ h.merge(k => send(k))
23
+ end
24
+ end
25
+
26
+ def errors_human_readable
27
+ return nil if @errors.empty?
28
+ missing_fields
29
+ end
30
+
31
+ def valid?
32
+ heroku_api_key.nil? && @errors.push([:missing, :heroku_api_key])
33
+ heroku_app_name.nil? && @errors.push([:missing, :heroku_app_name])
34
+ @errors.empty?
35
+ end
36
+
37
+ private
38
+
39
+ def initialize
40
+ self[:treshhold] = 0.5
41
+ @errors = []
42
+ end
43
+
44
+ def missing_fields
45
+ mf = @errors
46
+ .select { |etype, _| etype == :missing }
47
+ .map { |_, message| message }
48
+ %(Missing required_fields: #{mf.inspect})
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,3 @@
1
+ class RedisCloudAutoUpgrade
2
+ IllegalConfiguration = Class.new RuntimeError
3
+ end # class RedisCloudAutoUpgrade
@@ -0,0 +1,99 @@
1
+ require 'platform-api'
2
+ # A function wrapper accessing the Heroku Platform API in a functional way
3
+ class RedisCloudAutoUpgrade
4
+ module HerokuAPI
5
+ class << self
6
+ def available_memory(plan)
7
+ plan_memory
8
+ .fetch(plan) do
9
+ fail ArgumentError, "the plan #{plan.inspect} does not exist"
10
+ end
11
+ end
12
+
13
+ def current_redis_cloud_plan(heroku_api_key:, heroku_app_name:)
14
+ heroku(heroku_api_key)
15
+ .addon
16
+ .list_by_app(heroku_app_name)
17
+ .find(&select_redis_cloud_addon)['plan']['name']
18
+ end
19
+
20
+ def currently_available_memory(heroku_api_key:, heroku_app_name:)
21
+ available_memory(
22
+ current_redis_cloud_plan(
23
+ heroku_api_key: heroku_api_key, heroku_app_name: heroku_app_name))
24
+ end
25
+
26
+ def next_plan_to_upgrade_to(heroku_api_key:, heroku_app_name:)
27
+ next_plan(
28
+ current_redis_cloud_plan(
29
+ heroku_api_key: heroku_api_key, heroku_app_name: heroku_app_name))
30
+ end
31
+
32
+ def upgrade_plan!(heroku_api_key:, heroku_app_name:)
33
+ next_plan =
34
+ next_plan_to_upgrade_to(heroku_api_key: heroku_api_key, heroku_app_name: heroku_app_name)
35
+ heroku(heroku_api_key)
36
+ .addon
37
+ .update(heroku_app_name, 'rediscloud', plan: next_plan)
38
+ next_plan
39
+ end
40
+
41
+ private
42
+
43
+ def heroku(api_key)
44
+ PlatformAPI.connect api_key
45
+ end
46
+
47
+ def next_plan(plan)
48
+ plan_transitions
49
+ .fetch(plan) do
50
+ fail ArgumentError, "the plan #{plan.inspect} does not exist or cannot be upgraded"
51
+ end
52
+ end
53
+
54
+ # rubocop:disable Metrics/MethodLength
55
+ def plan_transitions
56
+ {
57
+ 'rediscloud:30' => 'rediscloud:100',
58
+ 'rediscloud:100' => 'rediscloud:250',
59
+ 'rediscloud:250' => 'rediscloud:500',
60
+ 'rediscloud:500' => 'rediscloud:1_000',
61
+ 'rediscloud:1_000' => 'rediscloud:2_500',
62
+ 'rediscloud:2_500' => 'rediscloud:5_000',
63
+ 'rediscloud:5_000' => 'rediscloud:10_000',
64
+ 'rediscloud:10_000' => 'rediscloud:15_000',
65
+ 'rediscloud:15_000' => 'rediscloud:20_000',
66
+ 'rediscloud:20_000' => 'rediscloud:25_000',
67
+ 'rediscloud:25_000' => 'rediscloud:50_000'
68
+ }
69
+ end
70
+
71
+ def plan_memory
72
+ {
73
+ 'rediscloud:30' => 30_000_000,
74
+ 'rediscloud:100' => 100_000_000,
75
+ 'rediscloud:250' => 250_000_000,
76
+ 'rediscloud:500' => 500_000_000,
77
+ 'rediscloud:1000' => 1_000_000_000,
78
+ 'rediscloud:2500' => 2_500_000_000,
79
+ 'rediscloud:5000' => 5_000_000_000,
80
+ 'rediscloud:10000' => 10_000_000_000,
81
+ 'rediscloud:15000' => 15_000_000_000,
82
+ 'rediscloud:20000' => 20_000_000_000,
83
+ 'rediscloud:25000' => 25_000_000_000,
84
+ 'rediscloud:50000' => 50_000_000_000
85
+ }
86
+ end
87
+
88
+ def select_redis_cloud_addon
89
+ lambda do |addon|
90
+ begin
91
+ addon['addon_service']['name'] == 'rediscloud'
92
+ rescue
93
+ nil
94
+ end
95
+ end
96
+ end
97
+ end # class << self
98
+ end # module HerokuAPI
99
+ end # class RCAU
@@ -0,0 +1,3 @@
1
+ class RedisCloudAutoUpgrade
2
+ VERSION = '0.2.2'
3
+ end # class RedisCloudAutoUpgrade
@@ -0,0 +1,121 @@
1
+ require_relative './redis_cloud_auto_upgrade/version'
2
+ require_relative './redis_cloud_auto_upgrade/exceptions'
3
+ require_relative './redis_cloud_auto_upgrade/configuration'
4
+
5
+ require_relative './redis_cloud_auto_upgrade/heroku_api'
6
+
7
+ require 'redis'
8
+
9
+ # See README.md for details
10
+ class RedisCloudAutoUpgrade
11
+ class << self
12
+ def potential_upgrade!(conf, &blk)
13
+ updated_conf = conf.merge(on_upgrade: blk)
14
+ new
15
+ .configure(updated_conf)
16
+ .potential_upgrade!
17
+ end
18
+ end # class << self
19
+
20
+ def configure(config)
21
+ @config.configure config
22
+ self
23
+ end
24
+
25
+ def current_redis_cloud_plan
26
+ @__current_redis_cloud_plan ||=
27
+ HerokuAPI.current_redis_cloud_plan(**heroku_params)
28
+ end
29
+
30
+ # Memoize from lab42_core gem?
31
+ def current_redis_mem_usage
32
+ return @__current_redis_mem_usage__ if @__current_redis_mem_usage__
33
+ redis_instance = config.redis_instance || Redis.current
34
+ @__current_redis_mem_usage__ = redis_instance.info['used_memory'].to_i
35
+ end
36
+
37
+ def needs_to_upgrade?
38
+ !(current_redis_mem_usage < currently_available_memory * config.treshhold)
39
+ end
40
+
41
+ def potential_upgrade!
42
+ if config.valid?
43
+ do_potential_upgrade!
44
+ else
45
+ fail IllegalConfiguration, config.errors_human_readable
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def initialize
52
+ @config = Configuration.new
53
+ end
54
+
55
+ attr_reader :config
56
+
57
+ def currently_available_memory
58
+ @__currently_available_memory__ ||=
59
+ HerokuAPI.currently_available_memory(
60
+ **config.only(:heroku_api_key, :heroku_app_name))
61
+ end
62
+
63
+ def do_potential_upgrade!
64
+ if needs_to_upgrade?
65
+ do_upgrade!
66
+ true
67
+ else
68
+ info "no upgrade needed #{config.heroku_app_name} mem usage #{current_redis_mem_usage / 1_000_000}MB"
69
+ false
70
+ end
71
+ end
72
+
73
+ def do_upgrade!
74
+ old_plan = current_redis_cloud_plan
75
+ new_plan = HerokuAPI.upgrade_plan!(
76
+ **config.only(:heroku_api_key, :heroku_app_name)
77
+ )
78
+ log_upgrade old_plan, new_plan
79
+ config.on_upgrade.call(update_data(old_plan, new_plan)) if config.on_upgrade
80
+ end
81
+
82
+ def log_upgrade(old_plan, new_plan)
83
+ info <<-EOS
84
+ upgraded RedisCloud plan for app: #{config.heroku_app_name}
85
+ mem usage was approximately #{current_redis_mem_usage / 1_000_000}MB
86
+ old_plan was #{old_plan}
87
+ new_plan is #{new_plan}
88
+ EOS
89
+ end
90
+
91
+ def heroku_params
92
+ @__heroku_params__ ||=
93
+ config.only(:heroku_api_key, :heroku_app_name)
94
+ end
95
+
96
+ def info(str)
97
+ return unless config.logger
98
+ config.logger.info str
99
+ end
100
+
101
+ def mem_usage_in_percent
102
+ @__mem_usage_in_percent__ ||=
103
+ current_redis_mem_usage * 100 / currently_available_memory
104
+ end
105
+
106
+ def update_data(old_plan, new_plan)
107
+ OpenStruct.new(
108
+ old_plan: old_plan,
109
+ new_plan: new_plan,
110
+ upgraded_at: Time.now,
111
+ mem_usage: current_redis_mem_usage,
112
+ mem_usage_in_percent: mem_usage_in_percent,
113
+ treshhold_in_percent: treshhold_in_percent
114
+ )
115
+ end
116
+
117
+ def treshhold_in_percent
118
+ @__treshhold_in_percent__ ||=
119
+ ( config.treshhold * 100 ).round
120
+ end
121
+ end # class RedisCloudAutoUpgrade
metadata ADDED
@@ -0,0 +1,164 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis_cloud_auto_upgrade
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - Robert Dober
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-03-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: platform-api
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry-byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.3'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 0.29.1
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 0.29.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: timecop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.8'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.8'
97
+ - !ruby/object:Gem::Dependency
98
+ name: vcr
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.24'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.24'
125
+ description: Gem permits to auto upgrade your Redis Cloud plan when a certain memory
126
+ usage treshold is reached
127
+ email: robert.dober@gmail.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - LICENSE
133
+ - README.md
134
+ - lib/redis_cloud_auto_upgrade.rb
135
+ - lib/redis_cloud_auto_upgrade/configuration.rb
136
+ - lib/redis_cloud_auto_upgrade/exceptions.rb
137
+ - lib/redis_cloud_auto_upgrade/heroku_api.rb
138
+ - lib/redis_cloud_auto_upgrade/version.rb
139
+ homepage: https://github.com/RobertDober/lab42_core
140
+ licenses:
141
+ - MIT
142
+ metadata: {}
143
+ post_install_message:
144
+ rdoc_options: []
145
+ require_paths:
146
+ - lib
147
+ required_ruby_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: 2.2.0
152
+ required_rubygems_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ requirements: []
158
+ rubyforge_project:
159
+ rubygems_version: 2.4.8
160
+ signing_key:
161
+ specification_version: 4
162
+ summary: Gem permits to auto upgrade your Redis Cloud plan when a certain memory usage
163
+ treshold is reached
164
+ test_files: []