Skip to content

Commit

Permalink
Merge aae86e1 into 1f2539c
Browse files Browse the repository at this point in the history
  • Loading branch information
sighphyre committed Feb 2, 2022
2 parents 1f2539c + aae86e1 commit b01609f
Show file tree
Hide file tree
Showing 14 changed files with 300 additions and 34 deletions.
30 changes: 29 additions & 1 deletion README.md
Expand Up @@ -79,6 +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` |
`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,7 +90,7 @@ For in a more in depth look, please see `lib/unleash/configuration.rb`.
require 'unleash'
require 'unleash/context'

@unleash = Unleash::Client.new(app_name: 'my_ruby_app', url: 'http://unleash.herokuapp.com/api', custom_http_headers: {'Authorization': '<API token>'})
@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 Down Expand Up @@ -267,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 Expand Up @@ -305,6 +327,12 @@ This client comes with the all the required strategies out of the box:
* UnknownStrategy
* UserWithIdStrategy

## Available Bootstrap Classes

This client comes with these classes to load unleash features on startup, before making a request to the Unleash API:

* Unleash::Bootstrap::FromFile
* Unleash::Bootstrap::FromUri

## Development

Expand Down
49 changes: 49 additions & 0 deletions examples/bootstrap.rb
@@ -0,0 +1,49 @@
#!/usr/bin/env ruby

require 'unleash'
require 'unleash/context'
require 'unleash/bootstrap/from_file'

puts ">> START bootstrap.rb"

@unleash = Unleash::Client.new(
url: 'http://unleash.herokuapp.com/api',
custom_http_headers: { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' },
app_name: 'bootstrap-test',
instance_id: 'local-test-cli',
refresh_interval: 2,
metrics_interval: 2,
retry_limit: 2,
bootstrap_data: Unleash::Bootstrap::FromFile.new('./examples/default-toggles.json').read
)

feature_name = "featureX"
unleash_context = Unleash::Context.new
unleash_context.user_id = 123

sleep 1
3.times do
if @unleash.is_enabled?(feature_name, unleash_context)
puts "> #{feature_name} is enabled"
else
puts "> #{feature_name} is not enabled"
end
sleep 1
puts "---"
puts ""
puts ""
end

sleep 3
feature_name = "foobar"
if @unleash.is_enabled?(feature_name, unleash_context, true)
puts "> #{feature_name} is enabled"
else
puts "> #{feature_name} is not enabled"
end

puts "> shutting down client..."

@unleash.shutdown

puts ">> END bootstrap.rb"
43 changes: 43 additions & 0 deletions examples/default-toggles.json
@@ -0,0 +1,43 @@

{
"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"
}
}
]

}
]}
12 changes: 12 additions & 0 deletions lib/unleash/bootstrap/base.rb
@@ -0,0 +1,12 @@
module Unleash
module Bootstrap
class NotImplemented < RuntimeError
end

class Base
def read
raise NotImplemented, "Bootstrap is not implemented"
end
end
end
end
10 changes: 10 additions & 0 deletions lib/unleash/bootstrap/from_file.rb
@@ -0,0 +1,10 @@
module Unleash
module Bootstrap
class FromFile < Base
# @param file_path [String]
def self.read(file_path)
File.read(file_path)
end
end
end
end
15 changes: 15 additions & 0 deletions lib/unleash/bootstrap/from_uri.rb
@@ -0,0 +1,15 @@
module Unleash
module Bootstrap
class FromUri < Base
# @param url [String]
# @param headers [Hash, nil] HTTP headers to use. If not set, the unleash client SDK ones will be used.
def self.read(url, headers = nil)
response = Unleash::Util::Http.get(URI.parse(url), nil, headers)

return nil if response.code != '200'

response.body
end
end
end
end
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: 3 additions & 1 deletion lib/unleash/configuration.rb
Expand Up @@ -18,7 +18,8 @@ class Configuration
:metrics_interval,
:backup_file,
:logger,
:log_level
:log_level,
:bootstrap_data

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

self.custom_http_headers = {}
end
Expand Down
38 changes: 28 additions & 10 deletions lib/unleash/toggle_fetcher.rb
Expand Up @@ -13,10 +13,14 @@ def initialize
self.toggle_resource = ConditionVariable.new
self.retry_count = 0

# start by fetching synchronously, and failing back to reading the backup file.
begin
fetch
# 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
# 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!
Expand All @@ -36,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 @@ -46,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 @@ -126,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
9 changes: 6 additions & 3 deletions lib/unleash/util/http.rb
Expand Up @@ -4,10 +4,10 @@
module Unleash
module Util
module Http
def self.get(uri, etag = nil)
def self.get(uri, etag = nil, headers_override = nil)
http = http_connection(uri)

request = Net::HTTP::Get.new(uri.request_uri, http_headers(etag))
request = Net::HTTP::Get.new(uri.request_uri, http_headers(etag, headers_override))

http.request(request)
end
Expand All @@ -30,10 +30,13 @@ def self.http_connection(uri)
http
end

def self.http_headers(etag = nil)
# @param etag [String, nil]
# @param headers_override [Hash, nil]
def self.http_headers(etag = nil, headers_override = nil)
Unleash.logger.debug "ETag: #{etag}" unless etag.nil?

headers = (Unleash.configuration.http_headers || {}).dup
headers = headers_override if headers_override.is_a?(Hash)
headers['Content-Type'] = 'application/json'
headers['If-None-Match'] = etag unless etag.nil?

Expand Down
14 changes: 14 additions & 0 deletions spec/unleash/bootstrap-resources/features-v1.json
@@ -0,0 +1,14 @@
{
"version": 1,
"features": [
{
"name": "featureX",
"enabled": true,
"strategies": [
{
"name": "default"
}
]
}
]
}
24 changes: 24 additions & 0 deletions spec/unleash/bootstrap/from_file_spec.rb
@@ -0,0 +1,24 @@
require 'spec_helper'
require 'rspec/json_expectations'
require 'unleash/bootstrap/base'
require 'unleash/bootstrap/from_file'
require 'json'

RSpec.describe Unleash::Bootstrap::FromFile do
before do
Unleash.configuration = Unleash::Configuration.new
Unleash.logger = Unleash.configuration.logger
end

it 'loads bootstrap toggle correctly from file' do
bootstrap_file = './spec/unleash/bootstrap-resources/features-v1.json'

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(bootstrap_features).to include_json(file_features)
end
end

0 comments on commit b01609f

Please sign in to comment.