unleash 0.1.1
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/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE +201 -0
- data/README.md +143 -0
- data/Rakefile +6 -0
- data/TODO.md +37 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/simple.rb +53 -0
- data/lib/unleash.rb +28 -0
- data/lib/unleash/activation_strategy.rb +17 -0
- data/lib/unleash/client.rb +74 -0
- data/lib/unleash/configuration.rb +68 -0
- data/lib/unleash/context.rb +18 -0
- data/lib/unleash/feature_toggle.rb +67 -0
- data/lib/unleash/metrics.rb +25 -0
- data/lib/unleash/metrics_reporter.rb +61 -0
- data/lib/unleash/scheduled_executor.rb +35 -0
- data/lib/unleash/strategy/application_hostname.rb +24 -0
- data/lib/unleash/strategy/base.rb +16 -0
- data/lib/unleash/strategy/default.rb +13 -0
- data/lib/unleash/strategy/gradual_rollout_random.rb +26 -0
- data/lib/unleash/strategy/gradual_rollout_sessionid.rb +20 -0
- data/lib/unleash/strategy/gradual_rollout_userid.rb +20 -0
- data/lib/unleash/strategy/remote_address.rb +18 -0
- data/lib/unleash/strategy/unknown.rb +13 -0
- data/lib/unleash/strategy/user_with_id.rb +18 -0
- data/lib/unleash/strategy/util.rb +16 -0
- data/lib/unleash/toggle_fetcher.rb +144 -0
- data/lib/unleash/version.rb +3 -0
- data/unleash-client.gemspec +30 -0
- metadata +135 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Unleash
|
4
|
+
module Strategy
|
5
|
+
class ApplicationHostname < Base
|
6
|
+
attr_accessor :hostname
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
self.hostname = Socket.gethostname || 'undefined'
|
10
|
+
end
|
11
|
+
|
12
|
+
def name
|
13
|
+
'applicationHostname'
|
14
|
+
end
|
15
|
+
|
16
|
+
# need: :params[:hostnames]
|
17
|
+
def is_enabled?(params = {}, context = nil)
|
18
|
+
return false unless params.is_a?(Hash) && params.has_key?('hostnames')
|
19
|
+
|
20
|
+
params['hostnames'].split(",").map(&:strip).map{|h| h.downcase }.include?(self.hostname)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Unleash
|
2
|
+
module Strategy
|
3
|
+
class NotImplemented < Exception
|
4
|
+
end
|
5
|
+
|
6
|
+
class Base
|
7
|
+
def name
|
8
|
+
raise NotImplemented, "Strategy is not implemented"
|
9
|
+
end
|
10
|
+
|
11
|
+
def is_enabled?(params = {}, context = nil)
|
12
|
+
raise NotImplemented, "Strategy is not implemented"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'unleash/strategy/util'
|
2
|
+
|
3
|
+
module Unleash
|
4
|
+
module Strategy
|
5
|
+
class GradualRolloutRandom < Base
|
6
|
+
def name
|
7
|
+
'gradualRolloutRandom'
|
8
|
+
end
|
9
|
+
|
10
|
+
# need: params['percentage']
|
11
|
+
def is_enabled?(params = {}, context = nil)
|
12
|
+
return false unless params.is_a?(Hash) && params.has_key?('percentage')
|
13
|
+
|
14
|
+
begin
|
15
|
+
percentage = Integer(params['percentage'] || 0)
|
16
|
+
rescue ArgumentError => e
|
17
|
+
return false
|
18
|
+
end
|
19
|
+
|
20
|
+
randomNumber = Random.rand(100) + 1
|
21
|
+
|
22
|
+
(percentage >= randomNumber)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'unleash/strategy/util'
|
2
|
+
|
3
|
+
module Unleash
|
4
|
+
module Strategy
|
5
|
+
class GradualRolloutSessionId < Base
|
6
|
+
def name
|
7
|
+
'gradualRolloutSessionId'
|
8
|
+
end
|
9
|
+
|
10
|
+
# need: params['percentage'], params['groupId'], context.user_id,
|
11
|
+
def is_enabled?(params = {}, context)
|
12
|
+
return false unless params.is_a?(Hash) && params.has_key?('percentage')
|
13
|
+
return false unless context.class.name == 'Unleash::Context'
|
14
|
+
|
15
|
+
percentage = Integer(params['percentage'] || 0)
|
16
|
+
(percentage > 0 && Util.get_normalized_number(context.session_id, params['groupId'] || "") <= percentage)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'unleash/strategy/util'
|
2
|
+
|
3
|
+
module Unleash
|
4
|
+
module Strategy
|
5
|
+
class GradualRolloutUserId < Base
|
6
|
+
def name
|
7
|
+
'gradualRolloutUserId'
|
8
|
+
end
|
9
|
+
|
10
|
+
# need: params['percentage'], params['groupId'], context.user_id,
|
11
|
+
def is_enabled?(params = {}, context)
|
12
|
+
return false unless params.is_a?(Hash) && params.has_key?('percentage')
|
13
|
+
return false unless context.class.name == 'Unleash::Context'
|
14
|
+
|
15
|
+
percentage = Integer(params['percentage'] || 0)
|
16
|
+
(percentage > 0 && Util.get_normalized_number(context.user_id, params['groupId'] || "") <= percentage)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Unleash
|
2
|
+
module Strategy
|
3
|
+
class RemoteAddress < Base
|
4
|
+
def name
|
5
|
+
'remoteAddress'
|
6
|
+
end
|
7
|
+
|
8
|
+
# need: params['ips'], context.remote_address
|
9
|
+
def is_enabled?(params = {}, context = nil)
|
10
|
+
return false unless params.is_a?(Hash) && params.has_key?('ips')
|
11
|
+
return false unless params.fetch('ips', nil).is_a? String
|
12
|
+
return false unless context.class.name == 'Unleash::Context'
|
13
|
+
|
14
|
+
params['ips'].split(',').map(&:strip).include?( context.remote_address )
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Unleash
|
2
|
+
module Strategy
|
3
|
+
class UserWithId < Base
|
4
|
+
def name
|
5
|
+
'userWithId'
|
6
|
+
end
|
7
|
+
|
8
|
+
# requires: params['userIds'], context.user_id,
|
9
|
+
def is_enabled?(params = {}, context = nil)
|
10
|
+
return false unless params.is_a?(Hash) && params.has_key?('userIds')
|
11
|
+
return false unless params.fetch('userIds', nil).is_a? String
|
12
|
+
return false unless context.class.name == 'Unleash::Context'
|
13
|
+
|
14
|
+
params['userIds'].split(",").map(&:strip).include?(context.user_id)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'murmurhash3'
|
2
|
+
|
3
|
+
module Unleash
|
4
|
+
module Strategy
|
5
|
+
module Util
|
6
|
+
module_function
|
7
|
+
|
8
|
+
NORMALIZER = 100
|
9
|
+
|
10
|
+
# convert the two strings () into a number between 1 and 100
|
11
|
+
def get_normalized_number(identifier, group_id)
|
12
|
+
MurmurHash3::V32.str_hash("#{group_id}:#{identifier}") % NORMALIZER + 1
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'unleash/configuration'
|
2
|
+
require 'unleash/scheduled_executor'
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
require 'thread'
|
6
|
+
|
7
|
+
module Unleash
|
8
|
+
|
9
|
+
class ToggleFetcher
|
10
|
+
attr_accessor :toggle_cache, :toggle_lock, :toggle_resource, :etag, :retry_count
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
self.etag = nil
|
14
|
+
self.toggle_cache = nil
|
15
|
+
self.toggle_lock = Mutex.new
|
16
|
+
self.toggle_resource = ConditionVariable.new
|
17
|
+
self.retry_count = 0
|
18
|
+
|
19
|
+
# start by fetching synchronously, and failing back to reading the backup file.
|
20
|
+
begin
|
21
|
+
fetch
|
22
|
+
rescue Exception => e
|
23
|
+
Unleash.logger.warn "ToggleFetcher was unable to fetch from the network, attempting to read from backup file."
|
24
|
+
read!
|
25
|
+
raise e
|
26
|
+
end
|
27
|
+
|
28
|
+
# once we have initialized, start the fetcher loop
|
29
|
+
scheduledExecutor = Unleash::ScheduledExecutor.new('ToggleFetcher', Unleash.configuration.refresh_interval)
|
30
|
+
scheduledExecutor.run { remote_toggles = fetch() }
|
31
|
+
end
|
32
|
+
|
33
|
+
def toggles
|
34
|
+
self.toggle_lock.synchronize do
|
35
|
+
# wait for resource, only if it is null
|
36
|
+
self.toggle_resource.wait(self.toggle_lock) if self.toggle_cache.nil?
|
37
|
+
return self.toggle_cache
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# rename to refresh_from_server! ??
|
42
|
+
def fetch
|
43
|
+
Unleash.logger.debug "fetch()"
|
44
|
+
Unleash.logger.debug "ETag: #{self.etag}" unless self.etag.nil?
|
45
|
+
|
46
|
+
uri = URI(Unleash.configuration.fetch_toggles_url)
|
47
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
48
|
+
http.open_timeout = Unleash.configuration.timeout # in seconds
|
49
|
+
http.read_timeout = Unleash.configuration.timeout # in seconds
|
50
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
51
|
+
request['If-None-Match'] = self.etag unless self.etag.nil?
|
52
|
+
|
53
|
+
response = http.request(request)
|
54
|
+
|
55
|
+
Unleash.logger.debug "No changes according to the unleash server, nothing to do." if response.code == '304'
|
56
|
+
return if response.code == '304'
|
57
|
+
|
58
|
+
raise IOError, "Unleash server returned a non 200/304 HTTP result." if response.code != '200'
|
59
|
+
|
60
|
+
self.etag = response['ETag']
|
61
|
+
response_hash = JSON.parse(response.body)
|
62
|
+
|
63
|
+
if response_hash['version'] == 1
|
64
|
+
features = response_hash['features']
|
65
|
+
else
|
66
|
+
raise NotImplemented, "Version of features provided by unleash server" \
|
67
|
+
" is unsupported by this client."
|
68
|
+
end
|
69
|
+
|
70
|
+
# always synchronize with the local cache when fetching:
|
71
|
+
synchronize_with_local_cache!(features)
|
72
|
+
|
73
|
+
Unleash.logger.info "Flush changes to running client variable"
|
74
|
+
update_client!
|
75
|
+
|
76
|
+
Unleash.logger.info "Saved to toggle cache, will save to disk now"
|
77
|
+
save!
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def synchronize_with_local_cache!(features)
|
83
|
+
if self.toggle_cache != features
|
84
|
+
self.toggle_lock.synchronize do
|
85
|
+
self.toggle_cache = features
|
86
|
+
end
|
87
|
+
|
88
|
+
# notify all threads waiting for this resource to no longer wait
|
89
|
+
self.toggle_resource.broadcast
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def update_client!
|
94
|
+
if Unleash.toggles != self.toggles
|
95
|
+
Unleash.logger.info "Updating toggles to main client, there has been a change in the server."
|
96
|
+
Unleash.toggles = self.toggles
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def backup_file_exists?
|
101
|
+
File.exists?(backup_file)
|
102
|
+
end
|
103
|
+
|
104
|
+
def save!
|
105
|
+
begin
|
106
|
+
file = File.open(Unleash.configuration.backup_file, "w")
|
107
|
+
|
108
|
+
self.toggle_lock.synchronize do
|
109
|
+
file.write(self.toggle_cache.to_json)
|
110
|
+
end
|
111
|
+
rescue Exception => e
|
112
|
+
# This is not really the end of the world. Swallowing the exception.
|
113
|
+
Unleash.logger.error "Unable to save backup file."
|
114
|
+
ensure
|
115
|
+
file.close unless file.nil?
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def read!
|
120
|
+
Unleash.logger.debug "read!()"
|
121
|
+
return nil unless File.exists?(Unleash.configuration.backup_file)
|
122
|
+
|
123
|
+
begin
|
124
|
+
file = File.open(Unleash.configuration.backup_file, "r")
|
125
|
+
line_cache = ""
|
126
|
+
file.each_line do |line|
|
127
|
+
line_cache += line
|
128
|
+
end
|
129
|
+
|
130
|
+
backup_as_hash = JSON.parse(line_cache)
|
131
|
+
synchronize_with_local_cache!(backup_as_hash)
|
132
|
+
|
133
|
+
rescue IOError => e
|
134
|
+
Unleash.logger.error "Unable to read the backup_file."
|
135
|
+
rescue JSON::ParserError => e
|
136
|
+
Unleash.logger.error "Unable to parse JSON from existing backup_file."
|
137
|
+
rescue Exception => e
|
138
|
+
Unleash.logger.error "Unable to extract valid data from backup_file."
|
139
|
+
ensure
|
140
|
+
file.close unless file.nil?
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'unleash/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "unleash"
|
8
|
+
spec.version = Unleash::VERSION
|
9
|
+
spec.authors = ["Renato Arruda"]
|
10
|
+
spec.email = ["rarruda@rarruda.org"]
|
11
|
+
spec.licenses = ["Apache-2.0"]
|
12
|
+
|
13
|
+
spec.summary = %q{Unleash feature toggle client.}
|
14
|
+
spec.description = %q{Unleash is a feature toggle system, that gives you a great overview
|
15
|
+
over all feature toggles across all your applications and services.}
|
16
|
+
spec.homepage = "https://github.com/unleash/unleash-client-ruby"
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
21
|
+
spec.bindir = "exe"
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_dependency "murmurhash3", "~> 0.1.6"
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
28
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
29
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: unleash
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Renato Arruda
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-06-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: murmurhash3
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.1.6
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.1.6
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.14'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.14'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
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.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
description: |-
|
70
|
+
Unleash is a feature toggle system, that gives you a great overview
|
71
|
+
over all feature toggles across all your applications and services.
|
72
|
+
email:
|
73
|
+
- rarruda@rarruda.org
|
74
|
+
executables: []
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- ".gitignore"
|
79
|
+
- ".rspec"
|
80
|
+
- ".travis.yml"
|
81
|
+
- Gemfile
|
82
|
+
- LICENSE
|
83
|
+
- README.md
|
84
|
+
- Rakefile
|
85
|
+
- TODO.md
|
86
|
+
- bin/console
|
87
|
+
- bin/setup
|
88
|
+
- examples/simple.rb
|
89
|
+
- lib/unleash.rb
|
90
|
+
- lib/unleash/activation_strategy.rb
|
91
|
+
- lib/unleash/client.rb
|
92
|
+
- lib/unleash/configuration.rb
|
93
|
+
- lib/unleash/context.rb
|
94
|
+
- lib/unleash/feature_toggle.rb
|
95
|
+
- lib/unleash/metrics.rb
|
96
|
+
- lib/unleash/metrics_reporter.rb
|
97
|
+
- lib/unleash/scheduled_executor.rb
|
98
|
+
- lib/unleash/strategy/application_hostname.rb
|
99
|
+
- lib/unleash/strategy/base.rb
|
100
|
+
- lib/unleash/strategy/default.rb
|
101
|
+
- lib/unleash/strategy/gradual_rollout_random.rb
|
102
|
+
- lib/unleash/strategy/gradual_rollout_sessionid.rb
|
103
|
+
- lib/unleash/strategy/gradual_rollout_userid.rb
|
104
|
+
- lib/unleash/strategy/remote_address.rb
|
105
|
+
- lib/unleash/strategy/unknown.rb
|
106
|
+
- lib/unleash/strategy/user_with_id.rb
|
107
|
+
- lib/unleash/strategy/util.rb
|
108
|
+
- lib/unleash/toggle_fetcher.rb
|
109
|
+
- lib/unleash/version.rb
|
110
|
+
- unleash-client.gemspec
|
111
|
+
homepage: https://github.com/unleash/unleash-client-ruby
|
112
|
+
licenses:
|
113
|
+
- Apache-2.0
|
114
|
+
metadata: {}
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
requirements: []
|
130
|
+
rubyforge_project:
|
131
|
+
rubygems_version: 2.6.14
|
132
|
+
signing_key:
|
133
|
+
specification_version: 4
|
134
|
+
summary: Unleash feature toggle client.
|
135
|
+
test_files: []
|