unleash 3.2.5 → 4.2.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.
@@ -32,23 +32,30 @@ module Unleash
32
32
  result
33
33
  end
34
34
 
35
- def get_variant(context, fallback_variant = disabled_variant)
35
+ def get_variant(context, fallback_variant = Unleash::FeatureToggle.disabled_variant)
36
36
  raise ArgumentError, "Provided fallback_variant is not of type Unleash::Variant" if fallback_variant.class.name != 'Unleash::Variant'
37
37
 
38
38
  context = ensure_valid_context(context)
39
39
 
40
- return disabled_variant unless self.enabled && am_enabled?(context, true)
41
- return disabled_variant if sum_variant_defs_weights <= 0
40
+ return Unleash::FeatureToggle.disabled_variant unless self.enabled && am_enabled?(context, true)
41
+ return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0
42
42
 
43
- variant = variant_from_override_match(context)
44
- variant = variant_from_weights(context) if variant.nil?
43
+ variant = variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness)
45
44
 
46
45
  Unleash.toggle_metrics.increment_variant(self.name, variant.name) unless Unleash.configuration.disable_metrics
47
46
  variant
48
47
  end
49
48
 
49
+ def self.disabled_variant
50
+ Unleash::Variant.new(name: 'disabled', enabled: false)
51
+ end
52
+
50
53
  private
51
54
 
55
+ def resolve_stickiness
56
+ self.variant_definitions&.map(&:stickiness)&.compact&.first || "default"
57
+ end
58
+
52
59
  # only check if it is enabled, do not do metrics
53
60
  def am_enabled?(context, default_result)
54
61
  result =
@@ -77,15 +84,12 @@ module Unleash
77
84
  strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) }
78
85
  end
79
86
 
80
- def disabled_variant
81
- Unleash::Variant.new(name: 'disabled', enabled: false)
82
- end
83
-
84
87
  def sum_variant_defs_weights
85
88
  self.variant_definitions.map(&:weight).reduce(0, :+)
86
89
  end
87
90
 
88
- def variant_salt(context)
91
+ def variant_salt(context, stickiness = "default")
92
+ return context.get_by_name(stickiness) unless stickiness == "default"
89
93
  return context.user_id unless context.user_id.to_s.empty?
90
94
  return context.session_id unless context.session_id.to_s.empty?
91
95
  return context.remote_address unless context.remote_address.to_s.empty?
@@ -100,8 +104,8 @@ module Unleash
100
104
  Unleash::Variant.new(name: variant.name, enabled: true, payload: variant.payload)
101
105
  end
102
106
 
103
- def variant_from_weights(context)
104
- variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context), self.name, sum_variant_defs_weights)
107
+ def variant_from_weights(context, stickiness)
108
+ variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context, stickiness), self.name, sum_variant_defs_weights)
105
109
  prev_weights = 0
106
110
 
107
111
  variant_definition = self.variant_definitions
@@ -110,7 +114,7 @@ module Unleash
110
114
  prev_weights += v.weight
111
115
  res
112
116
  end
113
- return disabled_variant if variant_definition.nil?
117
+ return self.disabled_variant if variant_definition.nil?
114
118
 
115
119
  Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload)
116
120
  end
@@ -135,7 +139,9 @@ module Unleash
135
139
  Constraint.new(
136
140
  c.fetch('contextName'),
137
141
  c.fetch('operator'),
138
- c.fetch('values')
142
+ c.fetch('values', nil) || c.fetch('value', nil),
143
+ inverted: c.fetch('inverted', false),
144
+ case_insensitive: c.fetch('caseInsensitive', false)
139
145
  )
140
146
  end
141
147
  )
@@ -150,6 +156,7 @@ module Unleash
150
156
  v.fetch('name', ''),
151
157
  v.fetch('weight', 0),
152
158
  v.fetch('payload', nil),
159
+ v.fetch('stickiness', nil),
153
160
  v.fetch('overrides', [])
154
161
  )
155
162
  end || []
@@ -6,6 +6,8 @@ require 'time'
6
6
 
7
7
  module Unleash
8
8
  class MetricsReporter
9
+ LONGEST_WITHOUT_A_REPORT = 600
10
+
9
11
  attr_accessor :last_time
10
12
 
11
13
  def initialize
@@ -33,16 +35,28 @@ module Unleash
33
35
  report
34
36
  end
35
37
 
36
- def send
37
- Unleash.logger.debug "send() Report"
38
+ def post
39
+ Unleash.logger.debug "post() Report"
40
+
41
+ if bucket_empty? && (Time.now - self.last_time < LONGEST_WITHOUT_A_REPORT) # and last time is less then 10 minutes...
42
+ Unleash.logger.debug "Report not posted to server, as it would have been empty. (and has been empty for up to 10 min)"
43
+
44
+ return
45
+ end
38
46
 
39
- response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_url, self.generate_report.to_json)
47
+ response = Unleash::Util::Http.post(Unleash.configuration.client_metrics_uri, self.generate_report.to_json)
40
48
 
41
49
  if ['200', '202'].include? response.code
42
- Unleash.logger.debug "Report sent to unleash server sucessfully. Server responded with http code #{response.code}"
50
+ Unleash.logger.debug "Report sent to unleash server successfully. Server responded with http code #{response.code}"
43
51
  else
44
52
  Unleash.logger.error "Error when sending report to unleash server. Server responded with http code #{response.code}."
45
53
  end
46
54
  end
55
+
56
+ private
57
+
58
+ def bucket_empty?
59
+ Unleash.toggle_metrics.features.empty?
60
+ end
47
61
  end
48
62
  end
@@ -1,19 +1,22 @@
1
1
  module Unleash
2
2
  class ScheduledExecutor
3
- attr_accessor :name, :interval, :max_exceptions, :retry_count, :thread
3
+ attr_accessor :name, :interval, :max_exceptions, :retry_count, :thread, :immediate_execution
4
4
 
5
- def initialize(name, interval, max_exceptions = 5)
5
+ def initialize(name, interval, max_exceptions = 5, immediate_execution = false)
6
6
  self.name = name || ''
7
7
  self.interval = interval
8
8
  self.max_exceptions = max_exceptions
9
9
  self.retry_count = 0
10
10
  self.thread = nil
11
+ self.immediate_execution = immediate_execution
11
12
  end
12
13
 
13
14
  def run(&blk)
14
15
  self.thread = Thread.new do
15
16
  Thread.current[:name] = self.name
16
17
 
18
+ run_blk{ blk.call } if self.immediate_execution
19
+
17
20
  Unleash.logger.debug "thread #{name} loop starting"
18
21
  loop do
19
22
  Unleash.logger.debug "thread #{name} sleeping for #{interval} seconds"
@@ -4,6 +4,7 @@ module Unleash
4
4
  module Strategy
5
5
  class ApplicationHostname < Base
6
6
  attr_accessor :hostname
7
+
7
8
  PARAM = 'hostnames'.freeze
8
9
 
9
10
  def initialize
@@ -38,16 +38,16 @@ module Unleash
38
38
 
39
39
  def resolve_stickiness(stickiness, context)
40
40
  case stickiness
41
- when 'userId'
42
- context.user_id
43
- when 'sessionId'
44
- context.session_id
45
41
  when 'random'
46
42
  random
47
43
  when 'default'
48
44
  context.user_id || context.session_id || random
49
45
  else
50
- nil
46
+ begin
47
+ context.get_by_name(stickiness)
48
+ rescue KeyError
49
+ nil
50
+ end
51
51
  end
52
52
  end
53
53
  end
@@ -13,7 +13,23 @@ module Unleash
13
13
  return false unless params.fetch(PARAM, nil).is_a? String
14
14
  return false unless context.class.name == 'Unleash::Context'
15
15
 
16
- params[PARAM].split(',').map(&:strip).include?(context.remote_address)
16
+ remote_address = ipaddr_or_nil_from_str(context.remote_address)
17
+
18
+ params[PARAM]
19
+ .split(',')
20
+ .map(&:strip)
21
+ .map{ |ipblock| ipaddr_or_nil_from_str(ipblock) }
22
+ .compact
23
+ .map{ |ipb| ipb.include? remote_address }
24
+ .any?
25
+ end
26
+
27
+ private
28
+
29
+ def ipaddr_or_nil_from_str(ip)
30
+ IPAddr.new(ip)
31
+ rescue StandardError
32
+ nil
17
33
  end
18
34
  end
19
35
  end
@@ -1,4 +1,5 @@
1
1
  require 'unleash/configuration'
2
+ require 'unleash/bootstrap/handler'
2
3
  require 'net/http'
3
4
  require 'json'
4
5
 
@@ -13,10 +14,15 @@ module Unleash
13
14
  self.toggle_resource = ConditionVariable.new
14
15
  self.retry_count = 0
15
16
 
16
- # start by fetching synchronously, and failing back to reading the backup file.
17
17
  begin
18
- fetch
18
+ # if bootstrap configuration is available, initialize. An immediate API read is also triggered
19
+ if Unleash.configuration.use_bootstrap?
20
+ bootstrap
21
+ else
22
+ fetch
23
+ end
19
24
  rescue StandardError => e
25
+ # fail back to reading the backup file
20
26
  Unleash.logger.warn "ToggleFetcher was unable to fetch from the network, attempting to read from backup file."
21
27
  Unleash.logger.debug "Exception Caught: #{e}"
22
28
  read!
@@ -36,7 +42,9 @@ module Unleash
36
42
  # rename to refresh_from_server! ??
37
43
  def fetch
38
44
  Unleash.logger.debug "fetch()"
39
- response = Unleash::Util::Http.get(Unleash.configuration.fetch_toggles_url, etag)
45
+ return if Unleash.configuration.disable_client
46
+
47
+ response = Unleash::Util::Http.get(Unleash.configuration.fetch_toggles_uri, etag)
40
48
 
41
49
  if response.code == '304'
42
50
  Unleash.logger.debug "No changes according to the unleash server, nothing to do."
@@ -46,14 +54,7 @@ module Unleash
46
54
  end
47
55
 
48
56
  self.etag = response['ETag']
49
- response_hash = JSON.parse(response.body)
50
-
51
- if response_hash['version'] >= 1
52
- features = response_hash['features']
53
- else
54
- raise NotImplemented, "Version of features provided by unleash server" \
55
- " is unsupported by this client."
56
- end
57
+ features = get_features(response.body)
57
58
 
58
59
  # always synchronize with the local cache when fetching:
59
60
  synchronize_with_local_cache!(features)
@@ -106,10 +107,11 @@ module Unleash
106
107
 
107
108
  def read!
108
109
  Unleash.logger.debug "read!()"
109
- return nil unless File.exist?(Unleash.configuration.backup_file)
110
+ backup_file = Unleash.configuration.backup_file
111
+ return nil unless File.exist?(backup_file)
110
112
 
111
113
  begin
112
- file = File.new(Unleash.configuration.backup_file, "r")
114
+ file = File.new(backup_file, "r")
113
115
  file_content = file.read
114
116
 
115
117
  backup_as_hash = JSON.parse(file_content)
@@ -125,5 +127,23 @@ module Unleash
125
127
  file&.close
126
128
  end
127
129
  end
130
+
131
+ def bootstrap
132
+ bootstrap_payload = Unleash::Bootstrap::Handler.new(Unleash.configuration.bootstrap_config).retrieve_toggles
133
+ synchronize_with_local_cache! get_features bootstrap_payload
134
+ update_running_client!
135
+
136
+ # reset Unleash.configuration.bootstrap_data to free up memory, as we will never use it again
137
+ Unleash.configuration.bootstrap_config = nil
138
+ end
139
+
140
+ # @param response_body [String]
141
+ def get_features(response_body)
142
+ response_hash = JSON.parse(response_body)
143
+ return response_hash['features'] if response_hash['version'] >= 1
144
+
145
+ raise NotImplemented, "Version of features provided by unleash server" \
146
+ " is unsupported by this client."
147
+ end
128
148
  end
129
149
  end
@@ -4,17 +4,15 @@ require 'uri'
4
4
  module Unleash
5
5
  module Util
6
6
  module Http
7
- def self.get(url, etag = nil)
8
- uri = URI(url)
7
+ def self.get(uri, etag = nil, headers_override = nil)
9
8
  http = http_connection(uri)
10
9
 
11
- request = Net::HTTP::Get.new(uri.request_uri, http_headers(etag))
10
+ request = Net::HTTP::Get.new(uri.request_uri, http_headers(etag, headers_override))
12
11
 
13
12
  http.request(request)
14
13
  end
15
14
 
16
- def self.post(url, body)
17
- uri = URI(url)
15
+ def self.post(uri, body)
18
16
  http = http_connection(uri)
19
17
 
20
18
  request = Net::HTTP::Post.new(uri.request_uri, http_headers)
@@ -32,10 +30,13 @@ module Unleash
32
30
  http
33
31
  end
34
32
 
35
- def self.http_headers(etag = nil)
33
+ # @param etag [String, nil]
34
+ # @param headers_override [Hash, nil]
35
+ def self.http_headers(etag = nil, headers_override = nil)
36
36
  Unleash.logger.debug "ETag: #{etag}" unless etag.nil?
37
37
 
38
38
  headers = (Unleash.configuration.http_headers || {}).dup
39
+ headers = headers_override if headers_override.is_a?(Hash)
39
40
  headers['Content-Type'] = 'application/json'
40
41
  headers['If-None-Match'] = etag unless etag.nil?
41
42
 
@@ -2,13 +2,13 @@ require 'unleash/variant_override'
2
2
 
3
3
  module Unleash
4
4
  class VariantDefinition
5
- attr_accessor :name, :weight, :payload, :overrides
5
+ attr_accessor :name, :weight, :payload, :overrides, :stickiness
6
6
 
7
- def initialize(name, weight = 0, payload = nil, overrides = [])
7
+ def initialize(name, weight = 0, payload = nil, stickiness = nil, overrides = [])
8
8
  self.name = name
9
9
  self.weight = weight
10
10
  self.payload = payload
11
- # self.overrides = overrides
11
+ self.stickiness = stickiness
12
12
  self.overrides = (overrides || [])
13
13
  .select{ |v| v.is_a?(Hash) && v.has_key?('contextName') }
14
14
  .map{ |v| VariantOverride.new(v.fetch('contextName', ''), v.fetch('values', [])) } || []
@@ -19,7 +19,8 @@ module Unleash
19
19
  end
20
20
 
21
21
  def to_s
22
- "<VariantDefinition: name=#{self.name},weight=#{self.weight},payload=#{self.payload},overrides=#{self.overrides}>"
22
+ "<VariantDefinition: name=#{self.name},weight=#{self.weight},payload=#{self.payload},stickiness=#{self.stickiness}" \
23
+ ",overrides=#{self.overrides}>"
23
24
  end
24
25
  end
25
26
  end
@@ -1,3 +1,3 @@
1
1
  module Unleash
2
- VERSION = "3.2.5".freeze
2
+ VERSION = "4.2.0".freeze
3
3
  end
data/lib/unleash.rb CHANGED
@@ -24,11 +24,6 @@ module Unleash
24
24
  attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :logger
25
25
  end
26
26
 
27
- def self.initialize
28
- self.toggles = []
29
- self.toggle_metrics = {}
30
- end
31
-
32
27
  # Support for configuration via yield:
33
28
  def self.configure
34
29
  self.configuration ||= Unleash::Configuration.new
@@ -31,6 +31,7 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency "rspec-json_expectations", "~> 2.2"
32
32
  spec.add_development_dependency "webmock", "~> 3.8"
33
33
 
34
- spec.add_development_dependency "coveralls", "~> 0.8"
35
34
  spec.add_development_dependency "rubocop", "~> 0.80"
35
+ spec.add_development_dependency "simplecov", "~> 0.21.2"
36
+ spec.add_development_dependency "simplecov-lcov", "~> 0.8.0"
36
37
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unleash
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.5
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Renato Arruda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-01 00:00:00.000000000 Z
11
+ date: 2022-03-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: murmurhash3
@@ -95,33 +95,47 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '3.8'
97
97
  - !ruby/object:Gem::Dependency
98
- name: coveralls
98
+ name: rubocop
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '0.8'
103
+ version: '0.80'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0.8'
110
+ version: '0.80'
111
111
  - !ruby/object:Gem::Dependency
112
- name: rubocop
112
+ name: simplecov
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '0.80'
117
+ version: 0.21.2
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: '0.80'
124
+ version: 0.21.2
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov-lcov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 0.8.0
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 0.8.0
125
139
  description: |-
126
140
  This is the ruby client for Unleash, a powerful feature toggle system
127
141
  that gives you a great overview over all feature toggles across all your applications and services.
@@ -132,10 +146,10 @@ executables:
132
146
  extensions: []
133
147
  extra_rdoc_files: []
134
148
  files:
149
+ - ".github/workflows/pull_request.yml"
135
150
  - ".gitignore"
136
151
  - ".rspec"
137
152
  - ".rubocop.yml"
138
- - ".travis.yml"
139
153
  - Gemfile
140
154
  - LICENSE
141
155
  - README.md
@@ -143,9 +157,16 @@ files:
143
157
  - bin/console
144
158
  - bin/setup
145
159
  - bin/unleash-client
160
+ - examples/bootstrap.rb
161
+ - examples/default-toggles.json
146
162
  - examples/simple.rb
147
163
  - lib/unleash.rb
148
164
  - lib/unleash/activation_strategy.rb
165
+ - lib/unleash/bootstrap/configuration.rb
166
+ - lib/unleash/bootstrap/handler.rb
167
+ - lib/unleash/bootstrap/provider/base.rb
168
+ - lib/unleash/bootstrap/provider/from_file.rb
169
+ - lib/unleash/bootstrap/provider/from_url.rb
149
170
  - lib/unleash/client.rb
150
171
  - lib/unleash/configuration.rb
151
172
  - lib/unleash/constraint.rb
@@ -190,7 +211,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
190
211
  - !ruby/object:Gem::Version
191
212
  version: '0'
192
213
  requirements: []
193
- rubygems_version: 3.2.3
214
+ rubygems_version: 3.3.6
194
215
  signing_key:
195
216
  specification_version: 4
196
217
  summary: Unleash feature toggle client.
data/.travis.yml DELETED
@@ -1,15 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - jruby
5
- - 3.0
6
- - 2.7
7
- - 2.6
8
- - 2.5
9
- before_install:
10
- - gem install bundler -v 2.1.4
11
- - git clone --depth 5 --branch v3.3.0 https://github.com/Unleash/client-specification.git client-specification
12
-
13
- notifications:
14
- slack:
15
- secure: x593zOjdl2yVB8uP54v8CmuCOat8GFHnK99NPvPHKvif5U7PGe0YOgYh4DC1+Jc9vfjn1ke+0++m+Gif4quowpeOaA/t45xpB494lyziXsBulYml245jRp9yzoUmIIt7KxHhv4rlo3Q1ztMJgh6a5yDCornKHW2bKTkLsvqVTwxBRatLOrt6K9O8FivO/NaqgcoXl7Rw0fOx/bsZtx2IAFueTCH19NoqW1mk9KFEZ96YqJSvuqmfDC0AO7siq03WKlB++nPlKe1QcrlPalCrcsSzrYNhYJ3akBTt/ZbE1v6YJv2L+zUqRnAPTY2H+qp8WejFQtdhIjfeJ/SWox0iWv/Wy/mTFfj+EhFO9Aq+xhMjJ1OOLtNAPoYJyatEVgJkILb6M26igTFcuI60xBbGNmh5ZYeyRdn5/xFb7G2zyJ2Swc3PvN1uLzMHfTF0R7WzGq4CRNGIOjrHTGncyB3IGAONOdJdM3iT9XKY6cdlRK0VkQjEsEMe0eNv2fxxLVSGna4sdJoTND6LhJ6qCfuS9DEDXwoRdLxAXxefycCh9VNp7gloMJx8IbHYxOW0BFZqc3hxNU9X2SwOj6j72DZMrdYDg2aPAW69HG0iMontQ37Di87JEW2F2Cpgb49+4twByrQNIx+st+DGNce1vpc0DN+KuJVdIcmha654lT7Ffe8=