Skip to content

Commit

Permalink
feat: improved logic, by simplifying.
Browse files Browse the repository at this point in the history
- by instead of passing an object, we pass it as a string, we simplify the code by a lot.
- bootstrap reading classes are much simpler now
  (borderline we should ask ourselves if we still want to bundle them at all, since the responsibility would then just live outside of the SDK)
- allow setting bootstrap configuration, and also disabling the toggle fetcher at the same time. Useful when testing.
- use as simple test resources as possible. If we don't need it for testing, then we shouldn't bundle it.
  (as it makes it confusing for whoever comes afterwards)
- added tests from the Client class perspective too, so we know that all steps in between work as expected.
  • Loading branch information
rarruda committed Jan 30, 2022
1 parent 092b8b4 commit bee998f
Show file tree
Hide file tree
Showing 12 changed files with 122 additions and 122 deletions.
33 changes: 23 additions & 10 deletions README.md
Expand Up @@ -79,7 +79,7 @@ Argument | Description | Required? | Type | Default Value|
`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` |
`logger` | Specify a custom `Logger` class to handle logs for the Unleash client. | N | Class | `Logger.new(STDOUT)` |
`log_level` | Change the log level for the `Logger` class. Constant from `Logger::Severity`. | N | Constant | `Logger::WARN` |
`bootstrapper` | Bootstrapper object to get a list of toggles on load before it reads them from the unleash server. This is useful for loading large states on startup without hitting the network. Bootstrapping classes are provided for URL and file reading but you can implement your own for other sources of toggles. | N | Unleash::Bootstrap::Base or Nil | `nil` |
`bootstrap_data` | Bootstrap data to be loaded on start-up. This is useful for loading large states on startup without (or before) hitting the network. | N | String | `nil` |

For in a more in depth look, please see `lib/unleash/configuration.rb`.

Expand All @@ -89,14 +89,8 @@ For in a more in depth look, please see `lib/unleash/configuration.rb`.
```ruby
require 'unleash'
require 'unleash/context'
require 'unleash/bootstrap'

@unleash = Unleash::Client.new(
app_name: 'my_ruby_app',
url: 'http://unleash.herokuapp.com/api',
custom_http_headers: { 'Authorization': '<API token>' },
bootstrapper: Unleash::Bootstrap::FromFile.new('./default-toggles.json')
)
@unleash = Unleash::Client.new(app_name: 'my_ruby_app', url: 'http://unleash.herokuapp.com/api', custom_http_headers: { 'Authorization': '<API token>' })

feature_name = "AwesomeFeature"
unleash_context = Unleash::Context.new
Expand All @@ -116,8 +110,6 @@ end
Put in `config/initializers/unleash.rb`:

```ruby
require 'unleash/bootstrap'

Unleash.configure do |config|
config.app_name = Rails.application.class.parent.to_s
config.url = 'http://unleash.herokuapp.com/api'
Expand Down Expand Up @@ -276,6 +268,27 @@ variant = UNLEASH.get_variant "ColorVariants", @unleash_context, fallback_varian
puts "variant color is: #{variant.payload.fetch('color')}"
```

#### Bootstrapping

`bootstrap_data` configuration allows the client to be initialized with a predefined set of toggle states.
The content of the parameter is a JSON string containing the response body from the unleash server.

We provide two classes to help fetch the bootstrap files:
* `Unleash::Bootstrap::FromFile`
* `Unleash::Bootstrap::FromUri`

Example usage:
```ruby
@unleash = Unleash::Client.new(
app_name: 'my_ruby_app',
url: 'http://unleash.herokuapp.com/api',
custom_http_headers: { 'Authorization': '<API token>' },
bootstrap_data: Unleash::Bootstrap::FromFile.new('./default-toggles.json').read
# or
# bootstrap_data: Unleash::Bootstrap::FromUri.new('https://example.com/unleash-default-toggles.json').read
)

```

#### Client methods

Expand Down
2 changes: 1 addition & 1 deletion examples/bootstrap.rb
Expand Up @@ -14,7 +14,7 @@
refresh_interval: 2,
metrics_interval: 2,
retry_limit: 2,
bootstrapper: Unleash::Bootstrap::FromFile.new('./examples/default-toggles.json')
bootstrap_data: Unleash::Bootstrap::FromFile.new('./examples/default-toggles.json').read
)

feature_name = "featureX"
Expand Down
6 changes: 0 additions & 6 deletions lib/unleash/bootstrap/base.rb
Expand Up @@ -7,12 +7,6 @@ class Base
def read
raise NotImplemented, "Bootstrap is not implemented"
end

def extract_features(bootstrap_hash)
raise NotImplemented, "The provided bootstrap data doesn't seem to have a valid set of toggles" if bootstrap_hash['version'] < 1

bootstrap_hash['features']
end
end
end
end
12 changes: 2 additions & 10 deletions lib/unleash/bootstrap/from_file.rb
@@ -1,17 +1,9 @@
module Unleash
module Bootstrap
class FromFile < Base
attr_accessor :file_path

# @param file_path [String]
def initialize(file_path)
self.file_path = file_path
end

def read
file_content = File.read(self.file_path)
bootstrap_hash = JSON.parse(file_content)
extract_features(bootstrap_hash)
def self.read(file_path)
File.read(file_path)
end
end
end
Expand Down
17 changes: 6 additions & 11 deletions lib/unleash/bootstrap/from_uri.rb
@@ -1,19 +1,14 @@
module Unleash
module Bootstrap
class FromUri < Base
attr_accessor :uri, :headers

# @param uri [String]
# @param url [String]
# @param headers [Hash, nil] HTTP headers to use. If not set, the unleash client SDK ones will be used.
def initialize(uri, headers = nil)
self.uri = URI(uri)
self.headers = headers
end
def self.read(url, headers = nil)
response = Unleash::Util::Http.get(URI.parse(url), nil, headers)

return nil if response.code != '200'

def read
response = Unleash::Util::Http.get(self.uri, nil, self.headers)
bootstrap_hash = JSON.parse(response.body)
extract_features(bootstrap_hash)
response.body
end
end
end
Expand Down
9 changes: 2 additions & 7 deletions lib/unleash/client.rb
Expand Up @@ -18,8 +18,9 @@ def initialize(*opts)
Unleash.logger = Unleash.configuration.logger.clone
Unleash.logger.level = Unleash.configuration.log_level

Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
if Unleash.configuration.disable_client
Unleash.logger.warn "Unleash::Client is disabled! Will only return default results!"
Unleash.logger.warn "Unleash::Client is disabled! Will only return default (or bootstrapped if available) results!"
return
end

Expand All @@ -37,11 +38,6 @@ def is_enabled?(feature, context = nil, default_value_param = false, &fallback_b
default_value_param
end

if Unleash.configuration.disable_client
Unleash.logger.warn "unleash_client is disabled! Always returning #{default_value} for feature #{feature}!"
return default_value
end

toggle_as_hash = Unleash&.toggles&.select{ |toggle| toggle['name'] == feature }&.first

if toggle_as_hash.nil?
Expand Down Expand Up @@ -121,7 +117,6 @@ def info
end

def start_toggle_fetcher
Unleash.toggle_fetcher = Unleash::ToggleFetcher.new
self.fetcher_scheduled_executor = Unleash::ScheduledExecutor.new(
'ToggleFetcher',
Unleash.configuration.refresh_interval,
Expand Down
4 changes: 2 additions & 2 deletions lib/unleash/configuration.rb
Expand Up @@ -19,7 +19,7 @@ class Configuration
:backup_file,
:logger,
:log_level,
:bootstrapper
:bootstrap_data

def initialize(opts = {})
ensure_valid_opts(opts)
Expand Down Expand Up @@ -93,7 +93,7 @@ def set_defaults
self.retry_limit = 5
self.backup_file = nil
self.log_level = Logger::WARN
self.bootstrapper = nil
self.bootstrap_data = nil

self.custom_http_headers = {}
end
Expand Down
49 changes: 30 additions & 19 deletions lib/unleash/toggle_fetcher.rb
Expand Up @@ -4,27 +4,24 @@

module Unleash
class ToggleFetcher
attr_accessor :toggle_cache, :toggle_lock, :toggle_resource, :etag, :retry_count, :bootstrapper
attr_accessor :toggle_cache, :toggle_lock, :toggle_resource, :etag, :retry_count

def initialize
self.etag = nil
self.toggle_cache = nil
self.toggle_lock = Mutex.new
self.toggle_resource = ConditionVariable.new
self.retry_count = 0
self.bootstrapper = Unleash.configuration.bootstrapper

# start by fetching synchronously, and failing back to reading the backup file.
begin
if self.bootstrapper.nil?
fetch
else
# if the consumer provides a bootstrapper, use it!
synchronize_with_local_cache! self.bootstrapper.read
update_running_client!
end
# if bootstrap configuration is available, initialize
bootstrap unless Unleash.configuration.bootstrap_data.nil?

# if the client is enabled, fetch synchronously
fetch unless Unleash.configuration.disable_client
rescue StandardError => e
Unleash.logger.warn "ToggleFetcher was unable to fetch from the network or bootstrap, attempting to read from backup file."
# fail back to reading the backup file
Unleash.logger.warn "ToggleFetcher was unable to fetch from the network, attempting to read from backup file."
Unleash.logger.debug "Exception Caught: #{e}"
read!
end
Expand All @@ -43,6 +40,8 @@ def toggles
# rename to refresh_from_server! ??
def fetch
Unleash.logger.debug "fetch()"
return if Unleash.configuration.disable_client

response = Unleash::Util::Http.get(Unleash.configuration.fetch_toggles_uri, etag)

if response.code == '304'
Expand All @@ -53,14 +52,7 @@ def fetch
end

self.etag = response['ETag']
response_hash = JSON.parse(response.body)

if response_hash['version'] >= 1
features = response_hash['features']
else
raise NotImplemented, "Version of features provided by unleash server" \
" is unsupported by this client."
end
features = get_features(response.body)

# always synchronize with the local cache when fetching:
synchronize_with_local_cache!(features)
Expand Down Expand Up @@ -133,5 +125,24 @@ def read!
file&.close
end
end

def bootstrap
features = get_features(Unleash.configuration.bootstrap_data)

synchronize_with_local_cache! features
update_running_client!

# reset Unleash.configuration.bootstrap_data to free up memory, as we will never use it again
Unleash.configuration.bootstrap_data = nil
end

# @param response_body [String]
def get_features(response_body)
response_hash = JSON.parse(response_body)
return response_hash['features'] if response_hash['version'] >= 1

raise NotImplemented, "Version of features provided by unleash server" \
" is unsupported by this client."
end
end
end
51 changes: 11 additions & 40 deletions spec/unleash/bootstrap-resources/features-v1.json
@@ -1,43 +1,14 @@

{
"version": 1,
"features": [
"version": 1,
"features": [
{
"name": "featureX",
"enabled": true,
"strategies": [
{
"name": "default"
}
]
},
{
"name": "featureY",
"enabled": false,
"strategies": [
{
"name": "baz",
"parameters": {
"foo": "bar"
}
}
]

},
{
"name": "featureZ",
"enabled": true,
"strategies": [
{
"name": "default"
},
{
"name": "hola",
"parameters": {
"name": "val"
}
}
]

"name": "featureX",
"enabled": true,
"strategies": [
{
"name": "default"
}
]
}
]}
]
}
6 changes: 4 additions & 2 deletions spec/unleash/bootstrap/from_file_spec.rb
Expand Up @@ -13,10 +13,12 @@
it 'loads bootstrap toggle correctly from file' do
bootstrap_file = './spec/unleash/bootstrap-resources/features-v1.json'

bootstrapper = Unleash::Bootstrap::FromFile.new(bootstrap_file)
bootstrap_contents = Unleash::Bootstrap::FromFile.read(bootstrap_file)
bootstrap_features = JSON.parse(bootstrap_contents)['features']

file_contents = File.open(bootstrap_file).read
file_features = JSON.parse(file_contents)['features']

expect(bootstrapper.read).to include_json(file_features)
expect(bootstrap_features).to include_json(file_features)
end
end
5 changes: 3 additions & 2 deletions spec/unleash/bootstrap/from_uri_spec.rb
Expand Up @@ -19,8 +19,9 @@
)
.to_return(status: 200, body: file_contents, headers: {})

bootstrapper = Unleash::Bootstrap::FromUri.new('http://test-url/bootstrap-goodness', {})
bootstrap_contents = Unleash::Bootstrap::FromUri.read('http://test-url/bootstrap-goodness', {})
bootstrap_features = JSON.parse(bootstrap_contents)['features']

expect(bootstrapper.read).to include_json(file_features)
expect(bootstrap_features).to include_json(file_features)
end
end

0 comments on commit bee998f

Please sign in to comment.