unleash 4.1.0 → 4.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/add-to-project.yml +14 -0
- data/.github/workflows/pull_request.yml +16 -10
- data/.gitignore +1 -1
- data/.rubocop.yml +4 -6
- data/README.md +133 -28
- data/examples/bootstrap.rb +1 -1
- data/examples/simple.rb +2 -2
- data/lib/unleash/client.rb +1 -1
- data/lib/unleash/configuration.rb +17 -10
- data/lib/unleash/constraint.rb +88 -10
- data/lib/unleash/context.rb +2 -1
- data/lib/unleash/feature_toggle.rb +9 -7
- data/lib/unleash/toggle_fetcher.rb +19 -30
- data/lib/unleash/version.rb +1 -1
- data/unleash-client.gemspec +1 -1
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f05a01bdf6013184b6948fb5c38f6b1da1cde9d18f9a0eeecd69b54cbed8ff18
|
4
|
+
data.tar.gz: 6343830fd945d00decd331acade205d605e0d496ed1d4afd4736affcb2f538ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1ac4a4b89f787b5ae27acda02865def0eb68f261da10b83c01a6b34f6706b1b29443375eb82418b9834db555fbe54c1a1dfe49e9bf4c761556145edce88f76c9
|
7
|
+
data.tar.gz: c7a2332c7e6ab54872b08432af4c5c2cf092b7586664eb0267e45e416fb9a72c46246dd6cdedf68b4a9421678e4cb9f9507365e40fc5c2113b9fba8e703a19dd
|
@@ -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:
|
@@ -18,7 +31,7 @@ jobs:
|
|
18
31
|
- jruby-9.2
|
19
32
|
- jruby-9.3
|
20
33
|
- 3.1
|
21
|
-
- 3.0
|
34
|
+
- '3.0'
|
22
35
|
- 2.7
|
23
36
|
- 2.6
|
24
37
|
- 2.5
|
@@ -33,14 +46,7 @@ jobs:
|
|
33
46
|
- name: Install dependencies
|
34
47
|
run: bundle install
|
35
48
|
- 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
|
49
|
+
run: git clone --depth 5 --branch v4.1.0 https://github.com/Unleash/client-specification.git client-specification
|
44
50
|
- name: Run tests
|
45
51
|
run: bundle exec rake
|
46
52
|
env:
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -7,8 +7,9 @@ Naming/PredicateName:
|
|
7
7
|
AllowedMethods:
|
8
8
|
- is_enabled?
|
9
9
|
|
10
|
+
|
10
11
|
Metrics/ClassLength:
|
11
|
-
Max:
|
12
|
+
Max: 125
|
12
13
|
Layout/LineLength:
|
13
14
|
Max: 140
|
14
15
|
Metrics/MethodLength:
|
@@ -16,13 +17,10 @@ Metrics/MethodLength:
|
|
16
17
|
Metrics/BlockLength:
|
17
18
|
Max: 110
|
18
19
|
Exclude:
|
19
|
-
- 'spec
|
20
|
-
- 'spec/unleash/client_spec.rb'
|
21
|
-
- 'spec/unleash/context_spec.rb'
|
22
|
-
- 'spec/unleash/feature_toggle_spec.rb'
|
20
|
+
- 'spec/**/*.rb'
|
23
21
|
|
24
22
|
Metrics/AbcSize:
|
25
|
-
Max:
|
23
|
+
Max: 30
|
26
24
|
Metrics/CyclomaticComplexity:
|
27
25
|
Max: 9
|
28
26
|
Metrics/PerceivedComplexity:
|
data/README.md
CHANGED
@@ -41,7 +41,7 @@ It is **required** to configure:
|
|
41
41
|
- `app_name` with the name of the runninng application.
|
42
42
|
- `custom_http_headers` with `{'Authorization': '<API token>'}` when using Unleash v4.0.0 and later.
|
43
43
|
|
44
|
-
Please substitute the example `'
|
44
|
+
Please substitute the example `'https://unleash.herokuapp.com/api'` for the url of your own instance.
|
45
45
|
|
46
46
|
It is **highly recommended** to configure:
|
47
47
|
- `instance_id` parameter with a unique identifier for the running instance.
|
@@ -50,7 +50,7 @@ It is **highly recommended** to configure:
|
|
50
50
|
```ruby
|
51
51
|
Unleash.configure do |config|
|
52
52
|
config.app_name = 'my_ruby_app'
|
53
|
-
config.url = '
|
53
|
+
config.url = 'https://unleash.herokuapp.com/api'
|
54
54
|
config.custom_http_headers = {'Authorization': '<API token>'}
|
55
55
|
end
|
56
56
|
```
|
@@ -58,7 +58,23 @@ end
|
|
58
58
|
or instantiate the client with the valid configuration:
|
59
59
|
|
60
60
|
```ruby
|
61
|
-
UNLEASH = Unleash::Client.new(url: '
|
61
|
+
UNLEASH = Unleash::Client.new(url: 'https://unleash.herokuapp.com/api', app_name: 'my_ruby_app', custom_http_headers: {'Authorization': '<API token>'})
|
62
|
+
```
|
63
|
+
|
64
|
+
## Dynamic custom HTTP headers
|
65
|
+
If you need custom HTTP headers that change during the lifetime of the client, the `custom_http_headers` can be given as a `Proc`.
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
Unleash.configure do |config|
|
69
|
+
config.app_name = 'my_ruby_app'
|
70
|
+
config.url = 'https://unleash.herokuapp.com/api'
|
71
|
+
config.custom_http_headers = proc do
|
72
|
+
{
|
73
|
+
'Authorization': '<API token>',
|
74
|
+
'X-Client-Request-Time': Time.now.iso8601
|
75
|
+
}
|
76
|
+
end
|
77
|
+
end
|
62
78
|
```
|
63
79
|
|
64
80
|
#### List of Arguments
|
@@ -74,9 +90,9 @@ Argument | Description | Required? | Type | Default Value|
|
|
74
90
|
`metrics_interval` | How often the unleash client should send metrics to server. | N | Integer | 60 |
|
75
91
|
`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
92
|
`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 | {} |
|
93
|
+
`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
94
|
`timeout` | How long to wait for the connection to be established or wait in reading state (open_timeout/read_timeout) | N | Integer | 30 |
|
79
|
-
`retry_limit` | How many consecutive failures in connecting to the Unleash server are allowed before giving up.
|
95
|
+
`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
96
|
`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
97
|
`logger` | Specify a custom `Logger` class to handle logs for the Unleash client. | N | Class | `Logger.new(STDOUT)` |
|
82
98
|
`log_level` | Change the log level for the `Logger` class. Constant from `Logger::Severity`. | N | Constant | `Logger::WARN` |
|
@@ -95,7 +111,7 @@ Environment Variable | Description
|
|
95
111
|
require 'unleash'
|
96
112
|
require 'unleash/context'
|
97
113
|
|
98
|
-
@unleash = Unleash::Client.new(app_name: 'my_ruby_app', url: '
|
114
|
+
@unleash = Unleash::Client.new(app_name: 'my_ruby_app', url: 'https://unleash.herokuapp.com/api', custom_http_headers: { 'Authorization': '<API token>' })
|
99
115
|
|
100
116
|
feature_name = "AwesomeFeature"
|
101
117
|
unleash_context = Unleash::Context.new
|
@@ -117,36 +133,104 @@ Put in `config/initializers/unleash.rb`:
|
|
117
133
|
```ruby
|
118
134
|
Unleash.configure do |config|
|
119
135
|
config.app_name = Rails.application.class.parent.to_s
|
120
|
-
config.url = '
|
136
|
+
config.url = 'https://unleash.herokuapp.com/api'
|
121
137
|
# config.instance_id = "#{Socket.gethostname}"
|
122
138
|
config.logger = Rails.logger
|
123
139
|
config.environment = Rails.env
|
124
140
|
end
|
125
141
|
|
126
142
|
UNLEASH = Unleash::Client.new
|
143
|
+
|
144
|
+
# Or if preferred:
|
145
|
+
# Rails.configuration.unleash = Unleash::Client.new
|
146
|
+
```
|
147
|
+
For `config.instance_id` use a string with a unique identification for the running instance.
|
148
|
+
For example: it could be the hostname, if you only run one App per host.
|
149
|
+
Or the docker container id, if you are running in docker.
|
150
|
+
If it is not set the client will generate an unique UUID for each execution.
|
151
|
+
|
152
|
+
To have it available in the `rails console` command as well, also add to the file above:
|
153
|
+
```ruby
|
154
|
+
Rails.application.console do
|
155
|
+
UNLEASH = Unleash::Client.new
|
156
|
+
# or
|
157
|
+
# Rails.configuration.unleash = Unleash::Client.new
|
158
|
+
end
|
127
159
|
```
|
128
|
-
For `config.instance_id` use a string with a unique identification for the running instance. For example: it could be the hostname, if you only run one App per host. Or the docker container id, if you are running in docker. If it is not set the client will generate an unique UUID for each execution.
|
129
160
|
|
161
|
+
#### Add Initializer if using [Puma in clustered mode](https://github.com/puma/puma#clustered-mode)
|
130
162
|
|
131
|
-
|
163
|
+
That is, multiple workers configured in `puma.rb`:
|
164
|
+
```ruby
|
165
|
+
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
|
166
|
+
```
|
167
|
+
|
168
|
+
##### with `preload_app!`
|
169
|
+
|
170
|
+
Then you may keep the client configuration still in `config/initializers/unleash.rb`:
|
171
|
+
```ruby
|
172
|
+
Unleash.configure do |config|
|
173
|
+
config.app_name = Rails.application.class.parent.to_s
|
174
|
+
config.environment = Rails.env
|
175
|
+
config.url = 'https://unleash.herokuapp.com/api'
|
176
|
+
config.custom_http_headers = {'Authorization': '<API token>'}
|
177
|
+
end
|
178
|
+
```
|
132
179
|
|
133
|
-
|
180
|
+
But you must ensure that the unleash client is instantiated only after the process is forked.
|
181
|
+
This is done by creating the client inside the `on_worker_boot` code block in `puma.rb` as below:
|
134
182
|
|
135
183
|
```ruby
|
184
|
+
#...
|
185
|
+
preload_app!
|
186
|
+
#...
|
187
|
+
|
136
188
|
on_worker_boot do
|
137
189
|
# ...
|
138
190
|
|
139
|
-
Unleash.
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
end
|
145
|
-
Rails.configuration.unleash = Unleash::Client.new
|
191
|
+
::UNLEASH = Unleash::Client.new
|
192
|
+
end
|
193
|
+
|
194
|
+
on_worker_shutdown do
|
195
|
+
::UNLEASH.shutdown
|
146
196
|
end
|
147
197
|
```
|
148
198
|
|
149
|
-
|
199
|
+
##### without `preload_app!`
|
200
|
+
|
201
|
+
By not using `preload_app!`:
|
202
|
+
- the `Rails` constant will NOT be available.
|
203
|
+
- but phased restarts will be possible.
|
204
|
+
|
205
|
+
You need to ensure that in `puma.rb`:
|
206
|
+
- loading unleash sdk with `require 'unleash'` explicitly, as it will not be pre-loaded.
|
207
|
+
- all parameters must be explicitly set in the `on_worker_boot` block, as `config/initializers/unleash.rb` is not read.
|
208
|
+
- there are no references to `Rails` constant, as that is not yet available.
|
209
|
+
|
210
|
+
Example for `puma.rb`:
|
211
|
+
```ruby
|
212
|
+
require 'unleash'
|
213
|
+
|
214
|
+
#...
|
215
|
+
# no preload_app!
|
216
|
+
|
217
|
+
on_worker_boot do
|
218
|
+
# ...
|
219
|
+
|
220
|
+
::UNLEASH = Unleash::Client.new(
|
221
|
+
app_name: 'my_rails_app',
|
222
|
+
environment: 'development',
|
223
|
+
url: 'https://unleash.herokuapp.com/api',
|
224
|
+
custom_http_headers: {'Authorization': '<API token>'},
|
225
|
+
)
|
226
|
+
end
|
227
|
+
|
228
|
+
on_worker_shutdown do
|
229
|
+
::UNLEASH.shutdown
|
230
|
+
end
|
231
|
+
```
|
232
|
+
|
233
|
+
Note that we also added shutdown hooks in `on_worker_shutdown`, to ensure a clean shutdown.
|
150
234
|
|
151
235
|
#### Add Initializer if using [Phusion Passenger](https://github.com/phusion/passenger)
|
152
236
|
|
@@ -162,7 +246,7 @@ PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
|
162
246
|
# config.instance_id = "#{Socket.gethostname}"
|
163
247
|
config.logger = Rails.logger
|
164
248
|
config.environment = Rails.env
|
165
|
-
config.url = '
|
249
|
+
config.url = 'https://unleash.herokuapp.com/api'
|
166
250
|
config.custom_http_headers = {'Authorization': '<API token>'}
|
167
251
|
end
|
168
252
|
|
@@ -171,6 +255,23 @@ PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
|
171
255
|
end
|
172
256
|
```
|
173
257
|
|
258
|
+
#### Add Initializer hooks when using within [Sidekiq](https://github.com/mperham/sidekiq)
|
259
|
+
|
260
|
+
Note that in this case we require that the code block for `Unleash.configure` is set beforehand.
|
261
|
+
For example in `config/initializers/unleash.rb`.
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
Sidekiq.configure_server do |config|
|
265
|
+
config.on(:startup) do
|
266
|
+
UNLEASH = Unleash::Client.new
|
267
|
+
end
|
268
|
+
|
269
|
+
config.on(:shutdown) do
|
270
|
+
UNLEASH.shutdown
|
271
|
+
end
|
272
|
+
end
|
273
|
+
```
|
274
|
+
|
174
275
|
#### Set Unleash::Context
|
175
276
|
|
176
277
|
Be sure to add the following method and callback in the application controller to have `@unleash_context` set for all requests:
|
@@ -210,7 +311,8 @@ if Rails.configuration.unleash.is_enabled? "AwesomeFeature", @unleash_context
|
|
210
311
|
end
|
211
312
|
```
|
212
313
|
|
213
|
-
If the feature is not found in the server, it will by default return false.
|
314
|
+
If the feature is not found in the server, it will by default return false.
|
315
|
+
However you can override that by setting the default return value to `true`:
|
214
316
|
|
215
317
|
```ruby
|
216
318
|
if UNLEASH.is_enabled? "AwesomeFeature", @unleash_context, true
|
@@ -275,14 +377,15 @@ puts "variant color is: #{variant.payload.fetch('color')}"
|
|
275
377
|
|
276
378
|
## Bootstrapping
|
277
379
|
|
278
|
-
Bootstrap configuration allows the client to be initialized with a predefined set of toggle states.
|
380
|
+
Bootstrap configuration allows the client to be initialized with a predefined set of toggle states.
|
381
|
+
Bootstrapping can be configured by providing a bootstrap configuration when initializing the client.
|
279
382
|
```ruby
|
280
383
|
@unleash = Unleash::Client.new(
|
281
|
-
url: '
|
384
|
+
url: 'https://unleash.herokuapp.com/api',
|
282
385
|
app_name: 'my_ruby_app',
|
283
386
|
custom_http_headers: { 'Authorization': '<API token>' },
|
284
387
|
bootstrap_config: Unleash::Bootstrap::Configuration.new({
|
285
|
-
url: "
|
388
|
+
url: "https://unleash.herokuapp.com/api/client/features",
|
286
389
|
url_headers: {'Authorization': '<API token>'}
|
287
390
|
})
|
288
391
|
)
|
@@ -295,7 +398,8 @@ The `Bootstrap::Configuration` initializer takes a hash with one of the followin
|
|
295
398
|
* `data` - A raw JSON string as returned by the Unleash server.
|
296
399
|
* `block` - A lambda containing custom logic if you need it, an example is provided below.
|
297
400
|
|
298
|
-
You should only specify one type of bootstrapping since only one will be invoked and the others will be ignored.
|
401
|
+
You should only specify one type of bootstrapping since only one will be invoked and the others will be ignored.
|
402
|
+
The order of preference is as follows:
|
299
403
|
|
300
404
|
- Select a data bootstrapper if it exists.
|
301
405
|
- If no data bootstrapper exists, select the block bootstrapper.
|
@@ -307,7 +411,7 @@ Example usage:
|
|
307
411
|
|
308
412
|
First saving the toggles locally:
|
309
413
|
```shell
|
310
|
-
curl -H 'Authorization: <API token>' -XGET '
|
414
|
+
curl -H 'Authorization: <API token>' -XGET 'https://unleash.herokuapp.com/api' > ./default-toggles.json
|
311
415
|
```
|
312
416
|
|
313
417
|
Now using them on start up:
|
@@ -320,15 +424,16 @@ custom_boostrapper = lambda {
|
|
320
424
|
|
321
425
|
@unleash = Unleash::Client.new(
|
322
426
|
app_name: 'my_ruby_app',
|
323
|
-
url: '
|
427
|
+
url: 'https://unleash.herokuapp.com/api',
|
324
428
|
custom_http_headers: { 'Authorization': '<API token>' },
|
325
429
|
bootstrap_config: Unleash::Bootstrap::Configuration.new({
|
326
430
|
block: custom_boostrapper
|
327
|
-
}
|
431
|
+
})
|
328
432
|
)
|
329
433
|
```
|
330
434
|
|
331
|
-
This example could be easily achieved with a file bootstrapper, this is just to illustrate the usage of custom bootstrapping.
|
435
|
+
This example could be easily achieved with a file bootstrapper, this is just to illustrate the usage of custom bootstrapping.
|
436
|
+
Be aware that the client initializer will block until bootstrapping is complete.
|
332
437
|
|
333
438
|
#### Client methods
|
334
439
|
|
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',
|
data/lib/unleash/client.rb
CHANGED
@@ -23,7 +23,7 @@ module Unleash
|
|
23
23
|
:bootstrap_config
|
24
24
|
|
25
25
|
def initialize(opts = {})
|
26
|
-
|
26
|
+
validate_custom_http_headers!(opts[:custom_http_headers]) if opts.has_key?(:custom_http_headers)
|
27
27
|
set_defaults
|
28
28
|
|
29
29
|
initialize_default_logger if opts[:logger].nil?
|
@@ -40,7 +40,8 @@ module Unleash
|
|
40
40
|
return if self.disable_client
|
41
41
|
|
42
42
|
raise ArgumentError, "URL and app_name are required parameters." if self.app_name.nil? || self.url.nil?
|
43
|
-
|
43
|
+
|
44
|
+
validate_custom_http_headers!(self.custom_http_headers)
|
44
45
|
end
|
45
46
|
|
46
47
|
def refresh_backup_file!
|
@@ -51,7 +52,7 @@ module Unleash
|
|
51
52
|
{
|
52
53
|
'UNLEASH-INSTANCEID' => self.instance_id,
|
53
54
|
'UNLEASH-APPNAME' => self.app_name
|
54
|
-
}.merge(
|
55
|
+
}.merge!(generate_custom_http_headers)
|
55
56
|
end
|
56
57
|
|
57
58
|
def fetch_toggles_uri
|
@@ -78,12 +79,6 @@ module Unleash
|
|
78
79
|
|
79
80
|
private
|
80
81
|
|
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
82
|
def set_defaults
|
88
83
|
self.app_name = nil
|
89
84
|
self.environment = 'default'
|
@@ -95,7 +90,7 @@ module Unleash
|
|
95
90
|
self.refresh_interval = 10
|
96
91
|
self.metrics_interval = 60
|
97
92
|
self.timeout = 30
|
98
|
-
self.retry_limit =
|
93
|
+
self.retry_limit = Float::INFINITY
|
99
94
|
self.backup_file = nil
|
100
95
|
self.log_level = Logger::WARN
|
101
96
|
self.bootstrap_config = nil
|
@@ -118,6 +113,18 @@ module Unleash
|
|
118
113
|
self
|
119
114
|
end
|
120
115
|
|
116
|
+
def validate_custom_http_headers!(custom_http_headers)
|
117
|
+
return if custom_http_headers.is_a?(Hash) || custom_http_headers.respond_to?(:call)
|
118
|
+
|
119
|
+
raise ArgumentError, "custom_http_headers must be a Hash or a Proc."
|
120
|
+
end
|
121
|
+
|
122
|
+
def generate_custom_http_headers
|
123
|
+
return self.custom_http_headers.call if self.custom_http_headers.respond_to?(:call)
|
124
|
+
|
125
|
+
self.custom_http_headers
|
126
|
+
end
|
127
|
+
|
121
128
|
def set_option(opt, val)
|
122
129
|
__send__("#{opt}=", val)
|
123
130
|
rescue NoMethodError
|
data/lib/unleash/constraint.rb
CHANGED
@@ -1,26 +1,104 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
1
3
|
module Unleash
|
2
4
|
class Constraint
|
3
|
-
attr_accessor :context_name, :operator, :
|
5
|
+
attr_accessor :context_name, :operator, :value, :inverted, :case_insensitive
|
6
|
+
|
7
|
+
OPERATORS = {
|
8
|
+
IN: ->(context_v, constraint_v){ constraint_v.include? context_v },
|
9
|
+
NOT_IN: ->(context_v, constraint_v){ !constraint_v.include? context_v },
|
10
|
+
STR_STARTS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.start_with? v } },
|
11
|
+
STR_ENDS_WITH: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.end_with? v } },
|
12
|
+
STR_CONTAINS: ->(context_v, constraint_v){ constraint_v.any?{ |v| context_v.include? v } },
|
13
|
+
NUM_EQ: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x - y).abs < Float::EPSILON } },
|
14
|
+
NUM_LT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x > y) } },
|
15
|
+
NUM_LTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x >= y) } },
|
16
|
+
NUM_GT: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x < y) } },
|
17
|
+
NUM_GTE: ->(context_v, constraint_v){ on_valid_float(constraint_v, context_v){ |x, y| (x <= y) } },
|
18
|
+
DATE_AFTER: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x < y) } },
|
19
|
+
DATE_BEFORE: ->(context_v, constraint_v){ on_valid_date(constraint_v, context_v){ |x, y| (x > y) } },
|
20
|
+
SEMVER_EQ: ->(context_v, constraint_v){ on_valid_version(constraint_v, context_v){ |x, y| (x == y) } },
|
21
|
+
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) } }
|
23
|
+
}.freeze
|
4
24
|
|
5
|
-
|
25
|
+
LIST_OPERATORS = [:IN, :NOT_IN, :STR_STARTS_WITH, :STR_ENDS_WITH, :STR_CONTAINS].freeze
|
6
26
|
|
7
|
-
def initialize(context_name, operator,
|
27
|
+
def initialize(context_name, operator, value = [], inverted: false, case_insensitive: false)
|
8
28
|
raise ArgumentError, "context_name is not a String" unless context_name.is_a?(String)
|
9
|
-
raise ArgumentError, "operator does not hold a valid value:" +
|
10
|
-
|
29
|
+
raise ArgumentError, "operator does not hold a valid value:" + OPERATORS.keys unless OPERATORS.include? operator.to_sym
|
30
|
+
|
31
|
+
self.validate_constraint_value_type(operator.to_sym, value)
|
11
32
|
|
12
33
|
self.context_name = context_name
|
13
|
-
self.operator = operator
|
14
|
-
self.
|
34
|
+
self.operator = operator.to_sym
|
35
|
+
self.value = value
|
36
|
+
self.inverted = !!inverted
|
37
|
+
self.case_insensitive = !!case_insensitive
|
15
38
|
end
|
16
39
|
|
17
40
|
def matches_context?(context)
|
18
|
-
Unleash.logger.debug "Unleash::Constraint matches_context?
|
41
|
+
Unleash.logger.debug "Unleash::Constraint matches_context? value: #{self.value} context.get_by_name(#{self.context_name})" \
|
19
42
|
" #{context.get_by_name(self.context_name)} "
|
43
|
+
match = matches_constraint?(context)
|
44
|
+
self.inverted ? !match : match
|
45
|
+
rescue KeyError
|
46
|
+
Unleash.logger.warn "Attemped to resolve a context key during constraint resolution: #{self.context_name} but it wasn't \
|
47
|
+
found on the context"
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.on_valid_date(val1, val2)
|
52
|
+
val1 = DateTime.parse(val1)
|
53
|
+
val2 = DateTime.parse(val2)
|
54
|
+
yield(val1, val2)
|
55
|
+
rescue ArgumentError
|
56
|
+
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
|
57
|
+
or constraint_value (#{val2}) into a date. Returning false!"
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.on_valid_float(val1, val2)
|
62
|
+
val1 = Float(val1)
|
63
|
+
val2 = Float(val2)
|
64
|
+
yield(val1, val2)
|
65
|
+
rescue ArgumentError
|
66
|
+
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
|
67
|
+
or constraint_value (#{val2}) into a number. Returning false!"
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.on_valid_version(val1, val2)
|
72
|
+
val1 = Gem::Version.new(val1)
|
73
|
+
val2 = Gem::Version.new(val2)
|
74
|
+
yield(val1, val2)
|
75
|
+
rescue ArgumentError
|
76
|
+
Unleash.logger.warn "Unleash::ConstraintMatcher unable to parse either context_value (#{val1}) \
|
77
|
+
or constraint_value (#{val2}) into a version. Return false!"
|
78
|
+
false
|
79
|
+
end
|
80
|
+
|
81
|
+
# This should be a private method but for some reason this fails on Ruby 2.5
|
82
|
+
def validate_constraint_value_type(operator, value)
|
83
|
+
raise ArgumentError, "context_name is not an Array" if LIST_OPERATORS.include?(operator) && value.is_a?(String)
|
84
|
+
raise ArgumentError, "context_name is not a String" if !LIST_OPERATORS.include?(operator) && value.is_a?(Array)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def matches_constraint?(context)
|
90
|
+
unless OPERATORS.include?(self.operator)
|
91
|
+
Unleash.logger.warn "Invalid constraint operator: #{self.operator}, this should be unreachable. Always returning false."
|
92
|
+
false
|
93
|
+
end
|
94
|
+
|
95
|
+
v = self.value.dup
|
96
|
+
context_value = context.get_by_name(self.context_name)
|
20
97
|
|
21
|
-
|
98
|
+
v.map!(&:upcase) if self.case_insensitive
|
99
|
+
context_value.upcase! if self.case_insensitive
|
22
100
|
|
23
|
-
operator
|
101
|
+
OPERATORS[self.operator].call(context_value, v)
|
24
102
|
end
|
25
103
|
end
|
26
104
|
end
|
data/lib/unleash/context.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Unleash
|
2
2
|
class Context
|
3
|
-
ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address].freeze
|
3
|
+
ATTRS = [:app_name, :environment, :user_id, :session_id, :remote_address, :current_time].freeze
|
4
4
|
|
5
5
|
attr_accessor(*[ATTRS, :properties].flatten)
|
6
6
|
|
@@ -12,6 +12,7 @@ module Unleash
|
|
12
12
|
self.user_id = value_for('userId', params)
|
13
13
|
self.session_id = value_for('sessionId', params)
|
14
14
|
self.remote_address = value_for('remoteAddress', params)
|
15
|
+
self.current_time = value_for('currentTime', params, Time.now.utc.iso8601.to_s)
|
15
16
|
|
16
17
|
properties = value_for('properties', params)
|
17
18
|
self.properties = properties.is_a?(Hash) ? properties.transform_keys(&:to_sym) : {}
|
@@ -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,7 +37,7 @@ module Unleash
|
|
37
37
|
|
38
38
|
context = ensure_valid_context(context)
|
39
39
|
|
40
|
-
return Unleash::FeatureToggle.disabled_variant unless self.enabled && am_enabled?(context
|
40
|
+
return Unleash::FeatureToggle.disabled_variant unless self.enabled && am_enabled?(context)
|
41
41
|
return Unleash::FeatureToggle.disabled_variant if sum_variant_defs_weights <= 0
|
42
42
|
|
43
43
|
variant = variant_from_override_match(context) || variant_from_weights(context, resolve_stickiness)
|
@@ -57,7 +57,7 @@ module Unleash
|
|
57
57
|
end
|
58
58
|
|
59
59
|
# only check if it is enabled, do not do metrics
|
60
|
-
def am_enabled?(context
|
60
|
+
def am_enabled?(context)
|
61
61
|
result =
|
62
62
|
if self.enabled
|
63
63
|
self.strategies.empty? ||
|
@@ -65,10 +65,10 @@ module Unleash
|
|
65
65
|
strategy_enabled?(s, context) && strategy_constraint_matches?(s, context)
|
66
66
|
end
|
67
67
|
else
|
68
|
-
|
68
|
+
false
|
69
69
|
end
|
70
70
|
|
71
|
-
Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled}
|
71
|
+
Unleash.logger.debug "Unleash::FeatureToggle (enabled:#{self.enabled} " \
|
72
72
|
"and Strategies combined with contraints returned #{result})"
|
73
73
|
|
74
74
|
result
|
@@ -139,7 +139,9 @@ module Unleash
|
|
139
139
|
Constraint.new(
|
140
140
|
c.fetch('contextName'),
|
141
141
|
c.fetch('operator'),
|
142
|
-
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)
|
143
145
|
)
|
144
146
|
end
|
145
147
|
)
|
@@ -65,24 +65,20 @@ module Unleash
|
|
65
65
|
|
66
66
|
def save!
|
67
67
|
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
68
|
|
72
|
-
|
73
|
-
|
69
|
+
backup_file = Unleash.configuration.backup_file
|
70
|
+
backup_file_tmp = "#{backup_file}.tmp"
|
71
|
+
|
72
|
+
self.toggle_lock.synchronize do
|
73
|
+
File.open(backup_file_tmp, "w") do |file|
|
74
74
|
file.write(self.toggle_cache.to_json)
|
75
|
-
file.close
|
76
|
-
File.rename(backup_file_tmp, backup_file)
|
77
75
|
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?
|
76
|
+
File.rename(backup_file_tmp, backup_file)
|
85
77
|
end
|
78
|
+
rescue StandardError => e
|
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}"
|
86
82
|
end
|
87
83
|
|
88
84
|
private
|
@@ -110,22 +106,15 @@ module Unleash
|
|
110
106
|
backup_file = Unleash.configuration.backup_file
|
111
107
|
return nil unless File.exist?(backup_file)
|
112
108
|
|
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
|
109
|
+
backup_as_hash = JSON.parse(File.read(backup_file))
|
110
|
+
synchronize_with_local_cache!(backup_as_hash)
|
111
|
+
update_running_client!
|
112
|
+
rescue IOError => e
|
113
|
+
Unleash.logger.error "Unable to read the backup_file: #{e}"
|
114
|
+
rescue JSON::ParserError => e
|
115
|
+
Unleash.logger.error "Unable to parse JSON from existing backup_file: #{e}"
|
116
|
+
rescue StandardError => e
|
117
|
+
Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown: #{e}"
|
129
118
|
end
|
130
119
|
|
131
120
|
def bootstrap
|
data/lib/unleash/version.rb
CHANGED
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.0.0"
|
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.
|
4
|
+
version: 4.3.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: 2022-
|
11
|
+
date: 2022-07-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: murmurhash3
|
@@ -98,16 +98,16 @@ dependencies:
|
|
98
98
|
name: rubocop
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
|
-
- - "
|
101
|
+
- - "<"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version:
|
103
|
+
version: 1.0.0
|
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.0.0
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: simplecov
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -146,6 +146,7 @@ executables:
|
|
146
146
|
extensions: []
|
147
147
|
extra_rdoc_files: []
|
148
148
|
files:
|
149
|
+
- ".github/workflows/add-to-project.yml"
|
149
150
|
- ".github/workflows/pull_request.yml"
|
150
151
|
- ".gitignore"
|
151
152
|
- ".rspec"
|