unleash 4.2.0 → 4.4.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 +4 -4
- data/.github/stale.yml +1 -0
- data/.github/workflows/add-to-project.yml +14 -0
- data/.github/workflows/pull_request.yml +20 -12
- data/.gitignore +1 -1
- data/.rspec +1 -0
- data/.rubocop.yml +6 -2
- data/README.md +75 -14
- data/examples/bootstrap.rb +1 -1
- data/examples/simple.rb +2 -2
- data/lib/unleash/activation_strategy.rb +3 -1
- data/lib/unleash/client.rb +15 -4
- data/lib/unleash/configuration.rb +22 -12
- data/lib/unleash/constraint.rb +22 -11
- data/lib/unleash/context.rb +8 -1
- data/lib/unleash/feature_toggle.rb +41 -24
- data/lib/unleash/metrics.rb +20 -12
- data/lib/unleash/strategies.rb +80 -0
- data/lib/unleash/strategy/flexible_rollout.rb +1 -1
- data/lib/unleash/strategy/gradual_rollout_sessionid.rb +1 -1
- data/lib/unleash/strategy/gradual_rollout_userid.rb +1 -1
- data/lib/unleash/strategy/remote_address.rb +1 -1
- data/lib/unleash/strategy/user_with_id.rb +1 -1
- data/lib/unleash/toggle_fetcher.rb +34 -34
- data/lib/unleash/variant_definition.rb +1 -1
- data/lib/unleash/variant_override.rb +1 -1
- data/lib/unleash/version.rb +1 -1
- data/lib/unleash.rb +11 -15
- data/unleash-client.gemspec +1 -1
- metadata +8 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e8461b30f4a58518d73731731a7dfbc32041a5a017362319b9d8ba76d2cecb98
|
4
|
+
data.tar.gz: '06098a3f106f50b0ddad46e17bef57584918ecbefe88486009d95edbdb546550'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ec0644f3bd93de7f75b06e44c059d39ff3ed87af00ee7ca17d1ad3a3e444626f190c4e26863167b116eda85ce0c01196542f8167f4494052fdae9b3e3e09e15c
|
7
|
+
data.tar.gz: 11764caf8ccd33c757cbd282a5e5f3d6d6b9dc7b70c29c0e9e196b512be3c1baee10abb21279358e7c7001a91b3825d5c42661910695e74e9e1585e2003c9b58
|
data/.github/stale.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
_extends: .github
|
@@ -5,8 +5,21 @@ on:
|
|
5
5
|
pull_request:
|
6
6
|
|
7
7
|
jobs:
|
8
|
-
|
8
|
+
lint:
|
9
|
+
name: RuboCop
|
10
|
+
timeout-minutes: 30
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
steps:
|
13
|
+
- uses: actions/checkout@v2
|
14
|
+
- name: Set up Ruby
|
15
|
+
uses: ruby/setup-ruby@v1
|
16
|
+
with:
|
17
|
+
ruby-version: "2.7"
|
18
|
+
bundler-cache: true
|
19
|
+
- name: Run RuboCop
|
20
|
+
run: bundle exec rubocop
|
9
21
|
|
22
|
+
test:
|
10
23
|
runs-on: ${{ matrix.os }}-latest
|
11
24
|
|
12
25
|
strategy:
|
@@ -15,16 +28,18 @@ jobs:
|
|
15
28
|
- ubuntu
|
16
29
|
- macos
|
17
30
|
ruby-version:
|
18
|
-
- jruby-9.
|
31
|
+
- jruby-9.4
|
19
32
|
- jruby-9.3
|
33
|
+
- jruby-9.2
|
34
|
+
- 3.2
|
20
35
|
- 3.1
|
21
|
-
- 3.0
|
36
|
+
- '3.0'
|
22
37
|
- 2.7
|
23
38
|
- 2.6
|
24
39
|
- 2.5
|
25
40
|
|
26
41
|
steps:
|
27
|
-
- uses: actions/checkout@
|
42
|
+
- uses: actions/checkout@v3
|
28
43
|
- name: Set up Ruby ${{ matrix.ruby-version }}
|
29
44
|
uses: ruby/setup-ruby@v1
|
30
45
|
with:
|
@@ -33,14 +48,7 @@ jobs:
|
|
33
48
|
- name: Install dependencies
|
34
49
|
run: bundle install
|
35
50
|
- name: Download test cases
|
36
|
-
run: git clone --depth 5 --branch v4.
|
37
|
-
- name: rubocop
|
38
|
-
uses: reviewdog/action-rubocop@v2
|
39
|
-
with:
|
40
|
-
github_token: ${{ secrets.GITHUB_TOKEN }}
|
41
|
-
rubocop_version: gemfile
|
42
|
-
rubocop_extensions: rubocop-rspec:gemfile
|
43
|
-
reporter: github-pr-review # Default is github-pr-check
|
51
|
+
run: git clone --depth 5 --branch v4.2.2 https://github.com/Unleash/client-specification.git client-specification
|
44
52
|
- name: Run tests
|
45
53
|
run: bundle exec rake
|
46
54
|
env:
|
data/.gitignore
CHANGED
data/.rspec
CHANGED
data/.rubocop.yml
CHANGED
@@ -6,10 +6,11 @@ AllCops:
|
|
6
6
|
Naming/PredicateName:
|
7
7
|
AllowedMethods:
|
8
8
|
- is_enabled?
|
9
|
+
- is_disabled?
|
9
10
|
|
10
11
|
|
11
12
|
Metrics/ClassLength:
|
12
|
-
Max:
|
13
|
+
Max: 135
|
13
14
|
Layout/LineLength:
|
14
15
|
Max: 140
|
15
16
|
Metrics/MethodLength:
|
@@ -22,7 +23,7 @@ Metrics/BlockLength:
|
|
22
23
|
Metrics/AbcSize:
|
23
24
|
Max: 30
|
24
25
|
Metrics/CyclomaticComplexity:
|
25
|
-
Max:
|
26
|
+
Max: 10
|
26
27
|
Metrics/PerceivedComplexity:
|
27
28
|
Max: 10
|
28
29
|
|
@@ -34,6 +35,9 @@ Style/StringLiterals:
|
|
34
35
|
Style/RedundantSelf:
|
35
36
|
Enabled: false
|
36
37
|
|
38
|
+
Style/OptionalBooleanParameter:
|
39
|
+
Enabled: false
|
40
|
+
|
37
41
|
Style/SymbolArray:
|
38
42
|
EnforcedStyle: brackets
|
39
43
|
Style/WordArray:
|
data/README.md
CHANGED
@@ -10,11 +10,13 @@ Leverage the [Unleash Server](https://github.com/Unleash/unleash) for powerful f
|
|
10
10
|
|
11
11
|
## Supported Ruby Interpreters
|
12
12
|
|
13
|
+
* MRI 3.2
|
13
14
|
* MRI 3.1
|
14
15
|
* MRI 3.0
|
15
16
|
* MRI 2.7
|
16
17
|
* MRI 2.6
|
17
18
|
* MRI 2.5
|
19
|
+
* jruby 9.4
|
18
20
|
* jruby 9.3
|
19
21
|
* jruby 9.2
|
20
22
|
|
@@ -23,7 +25,7 @@ Leverage the [Unleash Server](https://github.com/Unleash/unleash) for powerful f
|
|
23
25
|
Add this line to your application's Gemfile:
|
24
26
|
|
25
27
|
```ruby
|
26
|
-
gem 'unleash', '~> 4.
|
28
|
+
gem 'unleash', '~> 4.4.0'
|
27
29
|
```
|
28
30
|
|
29
31
|
And then execute:
|
@@ -41,7 +43,7 @@ It is **required** to configure:
|
|
41
43
|
- `app_name` with the name of the runninng application.
|
42
44
|
- `custom_http_headers` with `{'Authorization': '<API token>'}` when using Unleash v4.0.0 and later.
|
43
45
|
|
44
|
-
Please substitute the example `'
|
46
|
+
Please substitute the example `'https://unleash.herokuapp.com/api'` for the url of your own instance.
|
45
47
|
|
46
48
|
It is **highly recommended** to configure:
|
47
49
|
- `instance_id` parameter with a unique identifier for the running instance.
|
@@ -50,7 +52,7 @@ It is **highly recommended** to configure:
|
|
50
52
|
```ruby
|
51
53
|
Unleash.configure do |config|
|
52
54
|
config.app_name = 'my_ruby_app'
|
53
|
-
config.url = '
|
55
|
+
config.url = 'https://unleash.herokuapp.com/api'
|
54
56
|
config.custom_http_headers = {'Authorization': '<API token>'}
|
55
57
|
end
|
56
58
|
```
|
@@ -58,7 +60,23 @@ end
|
|
58
60
|
or instantiate the client with the valid configuration:
|
59
61
|
|
60
62
|
```ruby
|
61
|
-
UNLEASH = Unleash::Client.new(url: '
|
63
|
+
UNLEASH = Unleash::Client.new(url: 'https://unleash.herokuapp.com/api', app_name: 'my_ruby_app', custom_http_headers: {'Authorization': '<API token>'})
|
64
|
+
```
|
65
|
+
|
66
|
+
## Dynamic custom HTTP headers
|
67
|
+
If you need custom HTTP headers that change during the lifetime of the client, the `custom_http_headers` can be given as a `Proc`.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
Unleash.configure do |config|
|
71
|
+
config.app_name = 'my_ruby_app'
|
72
|
+
config.url = 'https://unleash.herokuapp.com/api'
|
73
|
+
config.custom_http_headers = proc do
|
74
|
+
{
|
75
|
+
'Authorization': '<API token>',
|
76
|
+
'X-Client-Request-Time': Time.now.iso8601
|
77
|
+
}
|
78
|
+
end
|
79
|
+
end
|
62
80
|
```
|
63
81
|
|
64
82
|
#### List of Arguments
|
@@ -74,13 +92,14 @@ Argument | Description | Required? | Type | Default Value|
|
|
74
92
|
`metrics_interval` | How often the unleash client should send metrics to server. | N | Integer | 60 |
|
75
93
|
`disable_client` | Disables all communication with the Unleash server, effectively taking it *offline*. If set, `is_enabled?` will always answer with the `default_value` and configuration validation is skipped. Defeats the entire purpose of using unleash, but can be useful in when running tests. | N | Boolean | `false` |
|
76
94
|
`disable_metrics` | Disables sending metrics to Unleash server. | N | Boolean | `false` |
|
77
|
-
`custom_http_headers` | Custom headers to send to Unleash. As of Unleash v4.0.0, the `Authorization` header is required. For example: `{'Authorization': '<API token>'}` | N | Hash | {} |
|
95
|
+
`custom_http_headers` | Custom headers to send to Unleash. As of Unleash v4.0.0, the `Authorization` header is required. For example: `{'Authorization': '<API token>'}` | N | Hash/Proc | {} |
|
78
96
|
`timeout` | How long to wait for the connection to be established or wait in reading state (open_timeout/read_timeout) | N | Integer | 30 |
|
79
97
|
`retry_limit` | How many consecutive failures in connecting to the Unleash server are allowed before giving up. The default is to retry indefinitely. | N | Float::INFINITY | 5 |
|
80
98
|
`backup_file` | Filename to store the last known state from the Unleash server. Best to not change this from the default. | N | String | `Dir.tmpdir + "/unleash-#{app_name}-repo.json` |
|
81
99
|
`logger` | Specify a custom `Logger` class to handle logs for the Unleash client. | N | Class | `Logger.new(STDOUT)` |
|
82
100
|
`log_level` | Change the log level for the `Logger` class. Constant from `Logger::Severity`. | N | Constant | `Logger::WARN` |
|
83
101
|
`bootstrap_config` | Bootstrap config on how to loaded data on start-up. This is useful for loading large states on startup without (or before) hitting the network. | N | Unleash::Bootstrap::Configuration | `nil` |
|
102
|
+
`strategies` | Strategies manager that holds all strategies and allows to add custom strategies | N | Unleash::Strategies | `Unleash::Strategies.new` |
|
84
103
|
|
85
104
|
For a more in-depth look, please see `lib/unleash/configuration.rb`.
|
86
105
|
|
@@ -95,7 +114,7 @@ Environment Variable | Description
|
|
95
114
|
require 'unleash'
|
96
115
|
require 'unleash/context'
|
97
116
|
|
98
|
-
@unleash = Unleash::Client.new(app_name: 'my_ruby_app', url: '
|
117
|
+
@unleash = Unleash::Client.new(app_name: 'my_ruby_app', url: 'https://unleash.herokuapp.com/api', custom_http_headers: { 'Authorization': '<API token>' })
|
99
118
|
|
100
119
|
feature_name = "AwesomeFeature"
|
101
120
|
unleash_context = Unleash::Context.new
|
@@ -106,6 +125,12 @@ if @unleash.is_enabled?(feature_name, unleash_context)
|
|
106
125
|
else
|
107
126
|
puts " #{feature_name} is disabled according to unleash"
|
108
127
|
end
|
128
|
+
|
129
|
+
if @unleash.is_disabled?(feature_name, unleash_context)
|
130
|
+
puts " #{feature_name} is disabled according to unleash"
|
131
|
+
else
|
132
|
+
puts " #{feature_name} is enabled according to unleash"
|
133
|
+
end
|
109
134
|
```
|
110
135
|
|
111
136
|
## Usage in a Rails Application
|
@@ -117,7 +142,7 @@ Put in `config/initializers/unleash.rb`:
|
|
117
142
|
```ruby
|
118
143
|
Unleash.configure do |config|
|
119
144
|
config.app_name = Rails.application.class.parent.to_s
|
120
|
-
config.url = '
|
145
|
+
config.url = 'https://unleash.herokuapp.com/api'
|
121
146
|
# config.instance_id = "#{Socket.gethostname}"
|
122
147
|
config.logger = Rails.logger
|
123
148
|
config.environment = Rails.env
|
@@ -156,7 +181,7 @@ Then you may keep the client configuration still in `config/initializers/unleash
|
|
156
181
|
Unleash.configure do |config|
|
157
182
|
config.app_name = Rails.application.class.parent.to_s
|
158
183
|
config.environment = Rails.env
|
159
|
-
config.url = '
|
184
|
+
config.url = 'https://unleash.herokuapp.com/api'
|
160
185
|
config.custom_http_headers = {'Authorization': '<API token>'}
|
161
186
|
end
|
162
187
|
```
|
@@ -204,7 +229,7 @@ on_worker_boot do
|
|
204
229
|
::UNLEASH = Unleash::Client.new(
|
205
230
|
app_name: 'my_rails_app',
|
206
231
|
environment: 'development',
|
207
|
-
url: '
|
232
|
+
url: 'https://unleash.herokuapp.com/api',
|
208
233
|
custom_http_headers: {'Authorization': '<API token>'},
|
209
234
|
)
|
210
235
|
end
|
@@ -230,7 +255,7 @@ PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
|
230
255
|
# config.instance_id = "#{Socket.gethostname}"
|
231
256
|
config.logger = Rails.logger
|
232
257
|
config.environment = Rails.env
|
233
|
-
config.url = '
|
258
|
+
config.url = 'https://unleash.herokuapp.com/api'
|
234
259
|
config.custom_http_headers = {'Authorization': '<API token>'}
|
235
260
|
end
|
236
261
|
|
@@ -285,6 +310,9 @@ Then wherever in your application that you need a feature toggle, you can use:
|
|
285
310
|
if UNLEASH.is_enabled? "AwesomeFeature", @unleash_context
|
286
311
|
puts "AwesomeFeature is enabled"
|
287
312
|
end
|
313
|
+
if UNLEASH.is_disabled? "AwesomeFeature", @unleash_context
|
314
|
+
puts "AwesomeFeature is disabled"
|
315
|
+
end
|
288
316
|
```
|
289
317
|
|
290
318
|
or if client is set in `Rails.configuration.unleash`:
|
@@ -293,6 +321,9 @@ or if client is set in `Rails.configuration.unleash`:
|
|
293
321
|
if Rails.configuration.unleash.is_enabled? "AwesomeFeature", @unleash_context
|
294
322
|
puts "AwesomeFeature is enabled"
|
295
323
|
end
|
324
|
+
if Rails.configuration.unleash.is_disabled? "AwesomeFeature", @unleash_context
|
325
|
+
puts "AwesomeFeature is enabled"
|
326
|
+
end
|
296
327
|
```
|
297
328
|
|
298
329
|
If the feature is not found in the server, it will by default return false.
|
@@ -365,11 +396,11 @@ Bootstrap configuration allows the client to be initialized with a predefined se
|
|
365
396
|
Bootstrapping can be configured by providing a bootstrap configuration when initializing the client.
|
366
397
|
```ruby
|
367
398
|
@unleash = Unleash::Client.new(
|
368
|
-
url: '
|
399
|
+
url: 'https://unleash.herokuapp.com/api',
|
369
400
|
app_name: 'my_ruby_app',
|
370
401
|
custom_http_headers: { 'Authorization': '<API token>' },
|
371
402
|
bootstrap_config: Unleash::Bootstrap::Configuration.new({
|
372
|
-
url: "
|
403
|
+
url: "https://unleash.herokuapp.com/api/client/features",
|
373
404
|
url_headers: {'Authorization': '<API token>'}
|
374
405
|
})
|
375
406
|
)
|
@@ -395,7 +426,7 @@ Example usage:
|
|
395
426
|
|
396
427
|
First saving the toggles locally:
|
397
428
|
```shell
|
398
|
-
curl -H 'Authorization: <API token>' -XGET '
|
429
|
+
curl -H 'Authorization: <API token>' -XGET 'https://unleash.herokuapp.com/api' > ./default-toggles.json
|
399
430
|
```
|
400
431
|
|
401
432
|
Now using them on start up:
|
@@ -408,7 +439,7 @@ custom_boostrapper = lambda {
|
|
408
439
|
|
409
440
|
@unleash = Unleash::Client.new(
|
410
441
|
app_name: 'my_ruby_app',
|
411
|
-
url: '
|
442
|
+
url: 'https://unleash.herokuapp.com/api',
|
412
443
|
custom_http_headers: { 'Authorization': '<API token>' },
|
413
444
|
bootstrap_config: Unleash::Bootstrap::Configuration.new({
|
414
445
|
block: custom_boostrapper
|
@@ -426,6 +457,9 @@ Method Name | Description | Return Type |
|
|
426
457
|
`is_enabled?` | Check if feature toggle is to be enabled or not. | Boolean |
|
427
458
|
`enabled?` | Alias to the `is_enabled?` method. But more ruby idiomatic. | Boolean |
|
428
459
|
`if_enabled` | Run a code block, if a feature is enabled. | `yield` |
|
460
|
+
`is_disabled?` | Check if feature toggle is to be enabled or not. | Boolean |
|
461
|
+
`disabled?` | Alias to the `is_disabled?` method. But more ruby idiomatic. | Boolean |
|
462
|
+
`if_disabled` | Run a code block, if a feature is disabled. | `yield` |
|
429
463
|
`get_variant` | Get variant for a given feature | `Unleash::Variant` |
|
430
464
|
`shutdown` | Save metrics to disk, flush metrics to server, and then kill ToggleFetcher and MetricsReporter threads. A safe shutdown. Not really useful in long running applications, like web applications. | nil |
|
431
465
|
`shutdown!` | Kill ToggleFetcher and MetricsReporter threads immediately. | nil |
|
@@ -456,10 +490,37 @@ This client comes with the all the required strategies out of the box:
|
|
456
490
|
* UnknownStrategy
|
457
491
|
* UserWithIdStrategy
|
458
492
|
|
493
|
+
## Custom Strategies
|
494
|
+
|
495
|
+
Client allows to add [custom activation strategies](https://docs.getunleash.io/advanced/custom_activation_strategy) using configuration.
|
496
|
+
In order for strategy to work correctly it should support two methods `name` and `is_enabled?`
|
497
|
+
|
498
|
+
```ruby
|
499
|
+
class MyCustomStrategy
|
500
|
+
def name
|
501
|
+
'muCustomStrategy'
|
502
|
+
end
|
503
|
+
|
504
|
+
def is_enabled?(params = {}, context = nil)
|
505
|
+
true
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
Unleash.configure do |config|
|
510
|
+
config.strategies.add(MyCustomStrategy.new)
|
511
|
+
end
|
512
|
+
```
|
513
|
+
|
459
514
|
## Development
|
460
515
|
|
461
516
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
462
517
|
|
518
|
+
This SDK is also built against the Unleash Client Specification tests. To run the Ruby SDK against this test suite, you'll need to have a copy on your machine, you can clone the repository directly using:
|
519
|
+
|
520
|
+
`git clone --depth 5 --branch v4.2.2 https://github.com/Unleash/client-specification.git client-specification`
|
521
|
+
|
522
|
+
After doing this, `rake spec` will also run the client specification tests.
|
523
|
+
|
463
524
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
464
525
|
|
465
526
|
|
data/examples/bootstrap.rb
CHANGED
@@ -7,7 +7,7 @@ require 'unleash/bootstrap/configuration'
|
|
7
7
|
puts ">> START bootstrap.rb"
|
8
8
|
|
9
9
|
@unleash = Unleash::Client.new(
|
10
|
-
url: '
|
10
|
+
url: 'https://unleash.herokuapp.com/api',
|
11
11
|
custom_http_headers: { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' },
|
12
12
|
app_name: 'bootstrap-test',
|
13
13
|
instance_id: 'local-test-cli',
|
data/examples/simple.rb
CHANGED
@@ -6,7 +6,7 @@ require 'unleash/context'
|
|
6
6
|
puts ">> START simple.rb"
|
7
7
|
|
8
8
|
# Unleash.configure do |config|
|
9
|
-
# config.url = '
|
9
|
+
# config.url = 'https://unleash.herokuapp.com/api'
|
10
10
|
# config.custom_http_headers = { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' }
|
11
11
|
# config.app_name = 'simple-test'
|
12
12
|
# config.refresh_interval = 2
|
@@ -18,7 +18,7 @@ puts ">> START simple.rb"
|
|
18
18
|
# or:
|
19
19
|
|
20
20
|
@unleash = Unleash::Client.new(
|
21
|
-
url: '
|
21
|
+
url: 'https://unleash.herokuapp.com/api',
|
22
22
|
custom_http_headers: { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' },
|
23
23
|
app_name: 'simple-test',
|
24
24
|
instance_id: 'local-test-cli',
|
@@ -1,9 +1,10 @@
|
|
1
1
|
module Unleash
|
2
2
|
class ActivationStrategy
|
3
|
-
attr_accessor :name, :params, :constraints
|
3
|
+
attr_accessor :name, :params, :constraints, :disabled
|
4
4
|
|
5
5
|
def initialize(name, params, constraints = [])
|
6
6
|
self.name = name
|
7
|
+
self.disabled = false
|
7
8
|
|
8
9
|
if params.is_a?(Hash)
|
9
10
|
self.params = params
|
@@ -18,6 +19,7 @@ module Unleash
|
|
18
19
|
self.constraints = constraints
|
19
20
|
else
|
20
21
|
Unleash.logger.warn "Invalid constraints provided for ActivationStrategy (contraints: #{constraints})"
|
22
|
+
self.disabled = true
|
21
23
|
self.constraints = []
|
22
24
|
end
|
23
25
|
end
|
data/lib/unleash/client.rb
CHANGED
@@ -12,7 +12,7 @@ module Unleash
|
|
12
12
|
attr_accessor :fetcher_scheduled_executor, :metrics_scheduled_executor
|
13
13
|
|
14
14
|
def initialize(*opts)
|
15
|
-
Unleash.configuration
|
15
|
+
Unleash.configuration = Unleash::Configuration.new(*opts) unless opts.empty?
|
16
16
|
Unleash.configuration.validate!
|
17
17
|
|
18
18
|
Unleash.logger = Unleash.configuration.logger.clone
|
@@ -45,19 +45,30 @@ module Unleash
|
|
45
45
|
return default_value
|
46
46
|
end
|
47
47
|
|
48
|
-
toggle = Unleash::FeatureToggle.new(toggle_as_hash)
|
48
|
+
toggle = Unleash::FeatureToggle.new(toggle_as_hash, Unleash&.segment_cache)
|
49
|
+
|
50
|
+
toggle.is_enabled?(context)
|
51
|
+
end
|
49
52
|
|
50
|
-
|
53
|
+
def is_disabled?(feature, context = nil, default_value_param = true, &fallback_blk)
|
54
|
+
!is_enabled?(feature, context, !default_value_param, &fallback_blk)
|
51
55
|
end
|
52
56
|
|
53
57
|
# enabled? is a more ruby idiomatic method name than is_enabled?
|
54
58
|
alias enabled? is_enabled?
|
59
|
+
# disabled? is a more ruby idiomatic method name than is_disabled?
|
60
|
+
alias disabled? is_disabled?
|
55
61
|
|
56
62
|
# execute a code block (passed as a parameter), if is_enabled? is true.
|
57
63
|
def if_enabled(feature, context = nil, default_value = false, &blk)
|
58
64
|
yield(blk) if is_enabled?(feature, context, default_value)
|
59
65
|
end
|
60
66
|
|
67
|
+
# execute a code block (passed as a parameter), if is_disabled? is true.
|
68
|
+
def if_disabled(feature, context = nil, default_value = true, &blk)
|
69
|
+
yield(blk) if is_disabled?(feature, context, default_value)
|
70
|
+
end
|
71
|
+
|
61
72
|
def get_variant(feature, context = Unleash::Context.new, fallback_variant = disabled_variant)
|
62
73
|
Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}"
|
63
74
|
|
@@ -110,7 +121,7 @@ module Unleash
|
|
110
121
|
'appName': Unleash.configuration.app_name,
|
111
122
|
'instanceId': Unleash.configuration.instance_id,
|
112
123
|
'sdkVersion': "unleash-client-ruby:" + Unleash::VERSION,
|
113
|
-
'strategies': Unleash
|
124
|
+
'strategies': Unleash.strategies.keys,
|
114
125
|
'started': Time.now.iso8601(Unleash::TIME_RESOLUTION),
|
115
126
|
'interval': Unleash.configuration.metrics_interval_in_millis
|
116
127
|
}
|
@@ -20,10 +20,11 @@ module Unleash
|
|
20
20
|
:backup_file,
|
21
21
|
:logger,
|
22
22
|
:log_level,
|
23
|
-
:bootstrap_config
|
23
|
+
:bootstrap_config,
|
24
|
+
:strategies
|
24
25
|
|
25
26
|
def initialize(opts = {})
|
26
|
-
|
27
|
+
validate_custom_http_headers!(opts[:custom_http_headers]) if opts.has_key?(:custom_http_headers)
|
27
28
|
set_defaults
|
28
29
|
|
29
30
|
initialize_default_logger if opts[:logger].nil?
|
@@ -40,7 +41,8 @@ module Unleash
|
|
40
41
|
return if self.disable_client
|
41
42
|
|
42
43
|
raise ArgumentError, "URL and app_name are required parameters." if self.app_name.nil? || self.url.nil?
|
43
|
-
|
44
|
+
|
45
|
+
validate_custom_http_headers!(self.custom_http_headers)
|
44
46
|
end
|
45
47
|
|
46
48
|
def refresh_backup_file!
|
@@ -50,8 +52,9 @@ module Unleash
|
|
50
52
|
def http_headers
|
51
53
|
{
|
52
54
|
'UNLEASH-INSTANCEID' => self.instance_id,
|
53
|
-
'UNLEASH-APPNAME' => self.app_name
|
54
|
-
|
55
|
+
'UNLEASH-APPNAME' => self.app_name,
|
56
|
+
'Unleash-Client-Spec' => '4.2.2'
|
57
|
+
}.merge!(generate_custom_http_headers)
|
55
58
|
end
|
56
59
|
|
57
60
|
def fetch_toggles_uri
|
@@ -78,12 +81,6 @@ module Unleash
|
|
78
81
|
|
79
82
|
private
|
80
83
|
|
81
|
-
def ensure_valid_opts(opts)
|
82
|
-
unless opts[:custom_http_headers].is_a?(Hash) || opts[:custom_http_headers].nil?
|
83
|
-
raise ArgumentError, "custom_http_headers must be a hash."
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
84
|
def set_defaults
|
88
85
|
self.app_name = nil
|
89
86
|
self.environment = 'default'
|
@@ -99,12 +96,13 @@ module Unleash
|
|
99
96
|
self.backup_file = nil
|
100
97
|
self.log_level = Logger::WARN
|
101
98
|
self.bootstrap_config = nil
|
99
|
+
self.strategies = Unleash::Strategies.new
|
102
100
|
|
103
101
|
self.custom_http_headers = {}
|
104
102
|
end
|
105
103
|
|
106
104
|
def initialize_default_logger
|
107
|
-
self.logger = Logger.new(
|
105
|
+
self.logger = Logger.new($stdout)
|
108
106
|
|
109
107
|
# on default logger, use custom formatter that includes thread_name:
|
110
108
|
self.logger.formatter = proc do |severity, datetime, _progname, msg|
|
@@ -118,6 +116,18 @@ module Unleash
|
|
118
116
|
self
|
119
117
|
end
|
120
118
|
|
119
|
+
def validate_custom_http_headers!(custom_http_headers)
|
120
|
+
return if custom_http_headers.is_a?(Hash) || custom_http_headers.respond_to?(:call)
|
121
|
+
|
122
|
+
raise ArgumentError, "custom_http_headers must be a Hash or a Proc."
|
123
|
+
end
|
124
|
+
|
125
|
+
def generate_custom_http_headers
|
126
|
+
return self.custom_http_headers.call if self.custom_http_headers.respond_to?(:call)
|
127
|
+
|
128
|
+
self.custom_http_headers
|
129
|
+
end
|
130
|
+
|
121
131
|
def set_option(opt, val)
|
122
132
|
__send__("#{opt}=", val)
|
123
133
|
rescue NoMethodError
|
data/lib/unleash/constraint.rb
CHANGED
@@ -1,12 +1,11 @@
|
|
1
1
|
require 'date'
|
2
|
-
|
3
2
|
module Unleash
|
4
3
|
class Constraint
|
5
4
|
attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive
|
6
5
|
|
7
6
|
OPERATORS = {
|
8
|
-
IN: ->(context_v, constraint_v){ constraint_v.include? context_v },
|
9
|
-
NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v },
|
7
|
+
IN: ->(context_v, constraint_v){ constraint_v.include? context_v.to_s },
|
8
|
+
NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v.to_s },
|
10
9
|
STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } },
|
11
10
|
STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } },
|
12
11
|
STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } },
|
@@ -19,16 +18,21 @@ module Unleash
|
|
19
18
|
DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } },
|
20
19
|
SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } },
|
21
20
|
SEMVER_GT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x < y) } },
|
22
|
-
SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } }
|
21
|
+
SEMVER_LT: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x > y) } },
|
22
|
+
FALLBACK_VALIDATOR: ->(_context_v, _constraint_v){ false }
|
23
23
|
}.freeze
|
24
24
|
|
25
25
|
LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
|
26
26
|
|
27
27
|
def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false)
|
28
28
|
raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String)
|
29
|
-
raise ArgumentError, "operator does not hold a valid value:" + OPERATORS.keys unless OPERATORS.include? operator.to_sym
|
30
29
|
|
31
|
-
|
30
|
+
unless OPERATORS.include? operator.to_sym
|
31
|
+
Unleash.logger.warn "Operator #{operator} is not a supported operator, " \
|
32
|
+
"falling back to FALLBACK_VALIDATOR which skips this constraint."
|
33
|
+
operator = "FALLBACK_VALIDATOR"
|
34
|
+
end
|
35
|
+
self.log_inconsistent_constraint_configuration(operator.to_sym, value)
|
32
36
|
|
33
37
|
self.context_name = context_name
|
34
38
|
self.operator = operator.to_sym
|
@@ -38,8 +42,9 @@ module Unleash
|
|
38
42
|
end
|
39
43
|
|
40
44
|
def matches_context?(context)
|
41
|
-
Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})"
|
42
|
-
|
45
|
+
Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})"
|
46
|
+
return false if context.nil?
|
47
|
+
|
43
48
|
match = matches_constraint?(context)
|
44
49
|
self.inverted ? !match : match
|
45
50
|
rescue KeyError
|
@@ -79,19 +84,25 @@ module Unleash
|
|
79
84
|
end
|
80
85
|
|
81
86
|
# This should be a private method but for some reason this fails on Ruby 2.5
|
82
|
-
def
|
83
|
-
|
84
|
-
|
87
|
+
def log_inconsistent_constraint_configuration(operator, value)
|
88
|
+
Unleash.logger.warn "value is a String, operator is expecting an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String)
|
89
|
+
Unleash.logger.warn "value is an Array, operator is expecting a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array)
|
85
90
|
end
|
86
91
|
|
87
92
|
private
|
88
93
|
|
89
94
|
def matches_constraint?(context)
|
95
|
+
Unleash.logger.debug "Unleash::Constraint matches_constraint? value: #{self.value} operator: #{self.operator} " \
|
96
|
+
" context.get_by_name(#{self.context_name})"
|
97
|
+
|
90
98
|
unless OPERATORS.include?(self.operator)
|
91
99
|
Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
|
92
100
|
false
|
93
101
|
end
|
94
102
|
|
103
|
+
# when the operator is NOT_IN and there is no data, return true. In all other cases the operator doesn't match.
|
104
|
+
return self.operator == :NOT_IN unless context.include?(self.context_name)
|
105
|
+
|
95
106
|
v = self.value.dup
|
96
107
|
context_value = context.get_by_name(self.context_name)
|
97
108
|
|
data/lib/unleash/context.rb
CHANGED
@@ -9,7 +9,7 @@ module Unleash
|
|
9
9
|
|
10
10
|
self.app_name = value_for('appName', params, Unleash&.configuration&.app_name)
|
11
11
|
self.environment = value_for('environment', params, Unleash&.configuration&.environment || 'default')
|
12
|
-
self.user_id = value_for('userId', params)
|
12
|
+
self.user_id = value_for('userId', params)&.to_s
|
13
13
|
self.session_id = value_for('sessionId', params)
|
14
14
|
self.remote_address = value_for('remoteAddress', params)
|
15
15
|
self.current_time = value_for('currentTime', params, Time.now.utc.iso8601.to_s)
|
@@ -33,6 +33,13 @@ module Unleash
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
+
def include?(name)
|
37
|
+
normalized_name = underscore(name)
|
38
|
+
return self.instance_variable_defined? "@#{normalized_name}" if ATTRS.include? normalized_name.to_sym
|
39
|
+
|
40
|
+
self.properties.include?(normalized_name.to_sym) || self.properties.include?(name.to_sym)
|
41
|
+
end
|
42
|
+
|
36
43
|
private
|
37
44
|
|
38
45
|
# Method to fetch values from hash for two types of keys: string in camelCase and symbol in snake_case
|
@@ -9,13 +9,13 @@ module Unleash
|
|
9
9
|
class FeatureToggle
|
10
10
|
attr_accessor :name, :enabled, :strategies, :variant_definitions
|
11
11
|
|
12
|
-
def initialize(params = {})
|
12
|
+
def initialize(params = {}, segment_map = {})
|
13
13
|
params = {} if params.nil?
|
14
14
|
|
15
15
|
self.name = params.fetch('name', nil)
|
16
16
|
self.enabled = params.fetch('enabled', false)
|
17
17
|
|
18
|
-
self.strategies = initialize_strategies(params)
|
18
|
+
self.strategies = initialize_strategies(params, segment_map)
|
19
19
|
self.variant_definitions = initialize_variant_definitions(params)
|
20
20
|
end
|
21
21
|
|
@@ -23,8 +23,8 @@ module Unleash
|
|
23
23
|
"<FeatureToggle: name=#{name},enabled=#{enabled},strategies=#{strategies},variant_definitions=#{variant_definitions}>"
|
24
24
|
end
|
25
25
|
|
26
|
-
def is_enabled?(context
|
27
|
-
result = am_enabled?(context
|
26
|
+
def is_enabled?(context)
|
27
|
+
result = am_enabled?(context)
|
28
28
|
|
29
29
|
choice = result ? :yes : :no
|
30
30
|
Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics
|
@@ -37,12 +37,11 @@ module Unleash
|
|
37
37
|
|
38
38
|
context = ensure_valid_context(context)
|
39
39
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
variant = variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness)
|
40
|
+
toggle_enabled = am_enabled?(context)
|
41
|
+
variant = resolve_variant(context, toggle_enabled)
|
44
42
|
|
45
|
-
|
43
|
+
choice = toggle_enabled ? :yes : :no
|
44
|
+
Unleash.toggle_metrics.increment_variant(self.name, choice, variant.name) unless Unleash.configuration.disable_metrics
|
46
45
|
variant
|
47
46
|
end
|
48
47
|
|
@@ -52,12 +51,19 @@ module Unleash
|
|
52
51
|
|
53
52
|
private
|
54
53
|
|
54
|
+
def resolve_variant(context, toggle_enabled)
|
55
|
+
return Unleash::FeatureToggle.disabled_variant unless toggle_enabled
|
56
|
+
return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0
|
57
|
+
|
58
|
+
variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness)
|
59
|
+
end
|
60
|
+
|
55
61
|
def resolve_stickiness
|
56
62
|
self.variant_definitions&.map(&:stickiness)&.compact&.first || "default"
|
57
63
|
end
|
58
64
|
|
59
65
|
# only check if it is enabled, do not do metrics
|
60
|
-
def am_enabled?(context
|
66
|
+
def am_enabled?(context)
|
61
67
|
result =
|
62
68
|
if self.enabled
|
63
69
|
self.strategies.empty? ||
|
@@ -65,22 +71,24 @@ module Unleash
|
|
65
71
|
strategy_enabled?(s, context) && strategy_constraint_matches?(s, context)
|
66
72
|
end
|
67
73
|
else
|
68
|
-
|
74
|
+
false
|
69
75
|
end
|
70
76
|
|
71
|
-
Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled}
|
77
|
+
Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} " \
|
72
78
|
"and Strategies combined with contraints returned #{result})"
|
73
79
|
|
74
80
|
result
|
75
81
|
end
|
76
82
|
|
77
83
|
def strategy_enabled?(strategy, context)
|
78
|
-
r = Unleash
|
84
|
+
r = Unleash.strategies.fetch(strategy.name).is_enabled?(strategy.params, context)
|
79
85
|
Unleash.logger.debug "Unleash::FeatureToggle.strategy_enabled? Strategy #{strategy.name} returned #{r} with context: #{context}"
|
80
86
|
r
|
81
87
|
end
|
82
88
|
|
83
89
|
def strategy_constraint_matches?(strategy, context)
|
90
|
+
return false if strategy.disabled
|
91
|
+
|
84
92
|
strategy.constraints.empty? || strategy.constraints.all?{ |c| c.matches_context?(context) }
|
85
93
|
end
|
86
94
|
|
@@ -128,26 +136,35 @@ module Unleash
|
|
128
136
|
context
|
129
137
|
end
|
130
138
|
|
131
|
-
def initialize_strategies(params)
|
139
|
+
def initialize_strategies(params, segment_map)
|
132
140
|
params.fetch('strategies', [])
|
133
|
-
.select{ |s| s.has_key?('name') && Unleash
|
141
|
+
.select{ |s| s.has_key?('name') && Unleash.strategies.includes?(s['name']) }
|
134
142
|
.map do |s|
|
135
143
|
ActivationStrategy.new(
|
136
144
|
s['name'],
|
137
145
|
s['parameters'],
|
138
|
-
(s
|
139
|
-
Constraint.new(
|
140
|
-
c.fetch('contextName'),
|
141
|
-
c.fetch('operator'),
|
142
|
-
c.fetch('values', nil) || c.fetch('value', nil),
|
143
|
-
inverted: c.fetch('inverted', false),
|
144
|
-
case_insensitive: c.fetch('caseInsensitive', false)
|
145
|
-
)
|
146
|
-
end
|
146
|
+
resolve_constraints(s, segment_map)
|
147
147
|
)
|
148
148
|
end || []
|
149
149
|
end
|
150
150
|
|
151
|
+
def resolve_constraints(strategy, segment_map)
|
152
|
+
segment_constraints = (strategy["segments"] || []).map do |segment_id|
|
153
|
+
segment_map[segment_id]&.fetch("constraints")
|
154
|
+
end
|
155
|
+
(strategy.fetch("constraints", []) + segment_constraints).flatten.map do |constraint|
|
156
|
+
return nil if constraint.nil?
|
157
|
+
|
158
|
+
Constraint.new(
|
159
|
+
constraint.fetch('contextName'),
|
160
|
+
constraint.fetch('operator'),
|
161
|
+
constraint.fetch('value', nil) || constraint.fetch('values', nil),
|
162
|
+
inverted: constraint.fetch('inverted', false),
|
163
|
+
case_insensitive: constraint.fetch('caseInsensitive', false)
|
164
|
+
)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
151
168
|
def initialize_variant_definitions(params)
|
152
169
|
(params.fetch('variants', []) || [])
|
153
170
|
.select{ |v| v.is_a?(Hash) && v.has_key?('name') }
|
data/lib/unleash/metrics.rb
CHANGED
@@ -1,33 +1,41 @@
|
|
1
1
|
module Unleash
|
2
2
|
class Metrics
|
3
|
-
attr_accessor :features
|
4
|
-
|
5
|
-
# NOTE: no mutexes for features
|
3
|
+
attr_accessor :features, :features_lock
|
6
4
|
|
7
5
|
def initialize
|
8
6
|
self.features = {}
|
7
|
+
self.features_lock = Mutex.new
|
9
8
|
end
|
10
9
|
|
11
10
|
def to_s
|
12
|
-
self.
|
11
|
+
self.features_lock.synchronize do
|
12
|
+
return self.features.to_json
|
13
|
+
end
|
13
14
|
end
|
14
15
|
|
15
16
|
def increment(feature, choice)
|
16
17
|
raise "InvalidArgument choice must be :yes or :no" unless [:yes, :no].include? choice
|
17
18
|
|
18
|
-
self.
|
19
|
-
|
19
|
+
self.features_lock.synchronize do
|
20
|
+
self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
|
21
|
+
self.features[feature][choice] += 1
|
22
|
+
end
|
20
23
|
end
|
21
24
|
|
22
|
-
def increment_variant(feature, variant)
|
23
|
-
self.
|
24
|
-
|
25
|
-
|
26
|
-
|
25
|
+
def increment_variant(feature, choice, variant)
|
26
|
+
self.features_lock.synchronize do
|
27
|
+
self.features[feature] = { yes: 0, no: 0 } unless self.features.include? feature
|
28
|
+
self.features[feature][choice] += 1
|
29
|
+
self.features[feature]['variant'] = {} unless self.features[feature].include? 'variant'
|
30
|
+
self.features[feature]['variant'][variant] = 0 unless self.features[feature]['variant'].include? variant
|
31
|
+
self.features[feature]['variant'][variant] += 1
|
32
|
+
end
|
27
33
|
end
|
28
34
|
|
29
35
|
def reset
|
30
|
-
self.
|
36
|
+
self.features_lock.synchronize do
|
37
|
+
self.features = {}
|
38
|
+
end
|
31
39
|
end
|
32
40
|
end
|
33
41
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'unleash/strategy/base'
|
2
|
+
Gem.find_files('unleash/strategy/**/*.rb').each{ |path| require path }
|
3
|
+
|
4
|
+
module Unleash
|
5
|
+
class Strategies
|
6
|
+
def initialize
|
7
|
+
@strategies = {}
|
8
|
+
register_strategies
|
9
|
+
end
|
10
|
+
|
11
|
+
def keys
|
12
|
+
@strategies.keys
|
13
|
+
end
|
14
|
+
|
15
|
+
def includes?(name)
|
16
|
+
@strategies.has_key?(name.to_s)
|
17
|
+
end
|
18
|
+
|
19
|
+
def fetch(name)
|
20
|
+
raise Unleash::Strategy::NotImplemented, "Strategy is not implemented" unless (strategy = @strategies[name.to_s])
|
21
|
+
|
22
|
+
strategy
|
23
|
+
end
|
24
|
+
|
25
|
+
def add(strategy)
|
26
|
+
@strategies[strategy.name] = strategy
|
27
|
+
end
|
28
|
+
|
29
|
+
def []=(key, strategy)
|
30
|
+
warn_deprecated_registration(strategy, 'modifying Unleash::STRATEGIES')
|
31
|
+
@strategies[key.to_s] = strategy
|
32
|
+
end
|
33
|
+
|
34
|
+
def [](key)
|
35
|
+
@strategies[key.to_s]
|
36
|
+
end
|
37
|
+
|
38
|
+
def register_strategies
|
39
|
+
register_base_strategies
|
40
|
+
register_custom_strategies
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
# Deprecated: Use Unleash.configuration to add custom strategies
|
46
|
+
def register_custom_strategies
|
47
|
+
Unleash::Strategy.constants
|
48
|
+
.select{ |c| Unleash::Strategy.const_get(c).is_a? Class }
|
49
|
+
.reject{ |c| ['NotImplemented', 'Base'].include?(c.to_s) } # Reject abstract classes
|
50
|
+
.map{ |c| Object.const_get("Unleash::Strategy::#{c}") }
|
51
|
+
.reject{ |c| DEFAULT_STRATEGIES.include?(c) } # Reject base classes
|
52
|
+
.each do |c|
|
53
|
+
strategy = c.new
|
54
|
+
warn_deprecated_registration(strategy, 'adding custom class into Unleash::Strategy namespace')
|
55
|
+
self.add(strategy)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def register_base_strategies
|
60
|
+
DEFAULT_STRATEGIES.each{ |c| self.add(c.new) }
|
61
|
+
end
|
62
|
+
|
63
|
+
DEFAULT_STRATEGIES = [
|
64
|
+
Unleash::Strategy::ApplicationHostname,
|
65
|
+
Unleash::Strategy::Default,
|
66
|
+
Unleash::Strategy::FlexibleRollout,
|
67
|
+
Unleash::Strategy::GradualRolloutRandom,
|
68
|
+
Unleash::Strategy::GradualRolloutSessionId,
|
69
|
+
Unleash::Strategy::GradualRolloutUserId,
|
70
|
+
Unleash::Strategy::RemoteAddress,
|
71
|
+
Unleash::Strategy::UserWithId
|
72
|
+
].freeze
|
73
|
+
|
74
|
+
def warn_deprecated_registration(strategy, method)
|
75
|
+
warn "[DEPRECATED] Registering custom Unleash strategy by #{method} is deprecated.
|
76
|
+
Please use Unleash configuration to register custom strategy: " \
|
77
|
+
"`Unleash.configure {|c| c.strategies.add(#{strategy.class.name}.new) }`"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -10,7 +10,7 @@ module Unleash
|
|
10
10
|
# need: params['percentage']
|
11
11
|
def is_enabled?(params = {}, context = nil)
|
12
12
|
return false unless params.is_a?(Hash)
|
13
|
-
return false unless context.
|
13
|
+
return false unless context.instance_of?(Unleash::Context)
|
14
14
|
|
15
15
|
stickiness = params.fetch('stickiness', 'default')
|
16
16
|
stickiness_id = resolve_stickiness(stickiness, context)
|
@@ -10,7 +10,7 @@ module Unleash
|
|
10
10
|
# need: params['percentage'], params['groupId'], context.user_id,
|
11
11
|
def is_enabled?(params = {}, context = nil)
|
12
12
|
return false unless params.is_a?(Hash) && params.has_key?('percentage')
|
13
|
-
return false unless context.
|
13
|
+
return false unless context.instance_of?(Unleash::Context)
|
14
14
|
return false if context.session_id.nil? || context.session_id.empty?
|
15
15
|
|
16
16
|
percentage = Integer(params['percentage'] || 0)
|
@@ -10,7 +10,7 @@ module Unleash
|
|
10
10
|
# need: params['percentage'], params['groupId'], context.user_id,
|
11
11
|
def is_enabled?(params = {}, context = nil, _constraints = [])
|
12
12
|
return false unless params.is_a?(Hash) && params.has_key?('percentage')
|
13
|
-
return false unless context.
|
13
|
+
return false unless context.instance_of?(Unleash::Context)
|
14
14
|
return false if context.user_id.nil? || context.user_id.empty?
|
15
15
|
|
16
16
|
percentage = Integer(params['percentage'] || 0)
|
@@ -11,7 +11,7 @@ module Unleash
|
|
11
11
|
def is_enabled?(params = {}, context = nil)
|
12
12
|
return false unless params.is_a?(Hash) && params.has_key?(PARAM)
|
13
13
|
return false unless params.fetch(PARAM, nil).is_a? String
|
14
|
-
return false unless context.
|
14
|
+
return false unless context.instance_of?(Unleash::Context)
|
15
15
|
|
16
16
|
remote_address = ipaddr_or_nil_from_str(context.remote_address)
|
17
17
|
|
@@ -11,7 +11,7 @@ module Unleash
|
|
11
11
|
def is_enabled?(params = {}, context = nil)
|
12
12
|
return false unless params.is_a?(Hash) && params.has_key?(PARAM)
|
13
13
|
return false unless params.fetch(PARAM, nil).is_a? String
|
14
|
-
return false unless context.
|
14
|
+
return false unless context.instance_of?(Unleash::Context)
|
15
15
|
|
16
16
|
params[PARAM].split(",").map(&:strip).include?(context.user_id)
|
17
17
|
end
|
@@ -5,11 +5,12 @@ require 'json'
|
|
5
5
|
|
6
6
|
module Unleash
|
7
7
|
class ToggleFetcher
|
8
|
-
attr_accessor :toggle_cache, :toggle_lock, :toggle_resource, :etag, :retry_count
|
8
|
+
attr_accessor :toggle_cache, :toggle_lock, :toggle_resource, :etag, :retry_count, :segment_cache
|
9
9
|
|
10
10
|
def initialize
|
11
11
|
self.etag = nil
|
12
12
|
self.toggle_cache = nil
|
13
|
+
self.segment_cache = nil
|
13
14
|
self.toggle_lock = Mutex.new
|
14
15
|
self.toggle_resource = ConditionVariable.new
|
15
16
|
self.retry_count = 0
|
@@ -65,24 +66,20 @@ module Unleash
|
|
65
66
|
|
66
67
|
def save!
|
67
68
|
Unleash.logger.debug "Will save toggles to disk now"
|
68
|
-
begin
|
69
|
-
backup_file = Unleash.configuration.backup_file
|
70
|
-
backup_file_tmp = "#{backup_file}.tmp"
|
71
69
|
|
72
|
-
|
73
|
-
|
70
|
+
backup_file = Unleash.configuration.backup_file
|
71
|
+
backup_file_tmp = "#{backup_file}.tmp"
|
72
|
+
|
73
|
+
self.toggle_lock.synchronize do
|
74
|
+
File.open(backup_file_tmp, "w") do |file|
|
74
75
|
file.write(self.toggle_cache.to_json)
|
75
|
-
file.close
|
76
|
-
File.rename(backup_file_tmp, backup_file)
|
77
76
|
end
|
78
|
-
|
79
|
-
# This is not really the end of the world. Swallowing the exception.
|
80
|
-
Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'"
|
81
|
-
Unleash.logger.error "stacktrace: #{e.backtrace}"
|
82
|
-
ensure
|
83
|
-
file&.close if defined?(file)
|
84
|
-
self.toggle_lock.unlock if self.toggle_lock.locked?
|
77
|
+
File.rename(backup_file_tmp, backup_file)
|
85
78
|
end
|
79
|
+
rescue StandardError => e
|
80
|
+
# This is not really the end of the world. Swallowing the exception.
|
81
|
+
Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'"
|
82
|
+
Unleash.logger.error "stacktrace: #{e.backtrace}"
|
86
83
|
end
|
87
84
|
|
88
85
|
private
|
@@ -99,9 +96,10 @@ module Unleash
|
|
99
96
|
end
|
100
97
|
|
101
98
|
def update_running_client!
|
102
|
-
if Unleash.toggles != self.toggles
|
99
|
+
if Unleash.toggles != self.toggles["features"] || Unleash.segment_cache != self.toggles["segments"]
|
103
100
|
Unleash.logger.info "Updating toggles to main client, there has been a change in the server."
|
104
|
-
Unleash.toggles = self.toggles
|
101
|
+
Unleash.toggles = self.toggles["features"]
|
102
|
+
Unleash.segment_cache = self.toggles["segments"]
|
105
103
|
end
|
106
104
|
end
|
107
105
|
|
@@ -110,22 +108,15 @@ module Unleash
|
|
110
108
|
backup_file = Unleash.configuration.backup_file
|
111
109
|
return nil unless File.exist?(backup_file)
|
112
110
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
rescue JSON::ParserError => e
|
123
|
-
Unleash.logger.error "Unable to parse JSON from existing backup_file: #{e}"
|
124
|
-
rescue StandardError => e
|
125
|
-
Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
|
126
|
-
ensure
|
127
|
-
file&.close
|
128
|
-
end
|
111
|
+
backup_as_hash = JSON.parse(File.read(backup_file))
|
112
|
+
synchronize_with_local_cache!(backup_as_hash)
|
113
|
+
update_running_client!
|
114
|
+
rescue IOError => e
|
115
|
+
Unleash.logger.error "Unable to read the backup_file: #{e}"
|
116
|
+
rescue JSON::ParserError => e
|
117
|
+
Unleash.logger.error "Unable to parse JSON from existing backup_file: #{e}"
|
118
|
+
rescue StandardError => e
|
119
|
+
Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
|
129
120
|
end
|
130
121
|
|
131
122
|
def bootstrap
|
@@ -137,10 +128,19 @@ module Unleash
|
|
137
128
|
Unleash.configuration.bootstrap_config = nil
|
138
129
|
end
|
139
130
|
|
131
|
+
def build_segment_map(segments_array)
|
132
|
+
return {} if segments_array.nil?
|
133
|
+
|
134
|
+
segments_array.map{ |segment| [segment["id"], segment] }.to_h
|
135
|
+
end
|
136
|
+
|
140
137
|
# @param response_body [String]
|
141
138
|
def get_features(response_body)
|
142
139
|
response_hash = JSON.parse(response_body)
|
143
|
-
|
140
|
+
|
141
|
+
if response_hash['version'] >= 1
|
142
|
+
return { "features" => response_hash["features"], "segments" => build_segment_map(response_hash["segments"]) }
|
143
|
+
end
|
144
144
|
|
145
145
|
raise NotImplemented, "Version of features provided by unleash server" \
|
146
146
|
" is unsupported by this client."
|
@@ -4,7 +4,7 @@ module Unleash
|
|
4
4
|
class VariantDefinition
|
5
5
|
attr_accessor :name, :weight, :payload, :overrides, :stickiness
|
6
6
|
|
7
|
-
def initialize(name, weight = 0, payload = nil, stickiness = nil, overrides = [])
|
7
|
+
def initialize(name, weight = 0, payload = nil, stickiness = nil, overrides = []) # rubocop:disable Metrics/ParameterLists
|
8
8
|
self.name = name
|
9
9
|
self.weight = weight
|
10
10
|
self.payload = payload
|
@@ -14,7 +14,7 @@ module Unleash
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def matches_context?(context)
|
17
|
-
raise ArgumentError, 'context must be of class Unleash::Context' unless context.
|
17
|
+
raise ArgumentError, 'context must be of class Unleash::Context' unless context.instance_of?(Unleash::Context)
|
18
18
|
|
19
19
|
context_value =
|
20
20
|
case self.context_name
|
data/lib/unleash/version.rb
CHANGED
data/lib/unleash.rb
CHANGED
@@ -1,35 +1,31 @@
|
|
1
1
|
require 'unleash/version'
|
2
2
|
require 'unleash/configuration'
|
3
|
-
require 'unleash/
|
3
|
+
require 'unleash/strategies'
|
4
4
|
require 'unleash/context'
|
5
5
|
require 'unleash/client'
|
6
6
|
require 'logger'
|
7
7
|
|
8
|
-
Gem.find_files('unleash/strategy/**/*.rb').each{ |path| require path unless path.end_with? '_spec.rb' }
|
9
|
-
|
10
8
|
module Unleash
|
11
9
|
TIME_RESOLUTION = 3
|
12
10
|
|
13
|
-
STRATEGIES = Unleash::Strategy.constants
|
14
|
-
.select{ |c| Unleash::Strategy.const_get(c).is_a? Class }
|
15
|
-
.reject{ |c| ['NotImplemented', 'Base'].include?(c.to_s) }
|
16
|
-
.map do |c|
|
17
|
-
lowered_c = c.to_s
|
18
|
-
lowered_c[0] = lowered_c[0].downcase
|
19
|
-
[lowered_c.to_sym, Object.const_get("Unleash::Strategy::#{c}").new]
|
20
|
-
end
|
21
|
-
.to_h
|
22
|
-
|
23
11
|
class << self
|
24
|
-
attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :logger
|
12
|
+
attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :segment_cache, :logger
|
25
13
|
end
|
26
14
|
|
15
|
+
self.configuration = Unleash::Configuration.new
|
16
|
+
|
17
|
+
# Deprecated: Use Unleash.configure to add custom strategies
|
18
|
+
STRATEGIES = self.configuration.strategies
|
19
|
+
|
27
20
|
# Support for configuration via yield:
|
28
21
|
def self.configure
|
29
|
-
self.configuration ||= Unleash::Configuration.new
|
30
22
|
yield(configuration)
|
31
23
|
|
32
24
|
self.configuration.validate!
|
33
25
|
self.configuration.refresh_backup_file!
|
34
26
|
end
|
27
|
+
|
28
|
+
def self.strategies
|
29
|
+
self.configuration.strategies
|
30
|
+
end
|
35
31
|
end
|
data/unleash-client.gemspec
CHANGED
@@ -31,7 +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 "rubocop", "~>
|
34
|
+
spec.add_development_dependency "rubocop", "~> 1.28.2"
|
35
35
|
spec.add_development_dependency "simplecov", "~> 0.21.2"
|
36
36
|
spec.add_development_dependency "simplecov-lcov", "~> 0.8.0"
|
37
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: 4.2
|
4
|
+
version: 4.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Renato Arruda
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-01-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: murmurhash3
|
@@ -100,14 +100,14 @@ dependencies:
|
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version:
|
103
|
+
version: 1.28.2
|
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:
|
110
|
+
version: 1.28.2
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: simplecov
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -146,6 +146,8 @@ executables:
|
|
146
146
|
extensions: []
|
147
147
|
extra_rdoc_files: []
|
148
148
|
files:
|
149
|
+
- ".github/stale.yml"
|
150
|
+
- ".github/workflows/add-to-project.yml"
|
149
151
|
- ".github/workflows/pull_request.yml"
|
150
152
|
- ".gitignore"
|
151
153
|
- ".rspec"
|
@@ -175,6 +177,7 @@ files:
|
|
175
177
|
- lib/unleash/metrics.rb
|
176
178
|
- lib/unleash/metrics_reporter.rb
|
177
179
|
- lib/unleash/scheduled_executor.rb
|
180
|
+
- lib/unleash/strategies.rb
|
178
181
|
- lib/unleash/strategy/application_hostname.rb
|
179
182
|
- lib/unleash/strategy/base.rb
|
180
183
|
- lib/unleash/strategy/default.rb
|
@@ -211,7 +214,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
211
214
|
- !ruby/object:Gem::Version
|
212
215
|
version: '0'
|
213
216
|
requirements: []
|
214
|
-
rubygems_version: 3.3.
|
217
|
+
rubygems_version: 3.3.5
|
215
218
|
signing_key:
|
216
219
|
specification_version: 4
|
217
220
|
summary: Unleash feature toggle client.
|