unleash-0.1.6/0000755000175000017500000000000013570535542011702 5ustar srudsrudunleash-0.1.6/.rubocop.yml0000644000175000017500000000040513570535542014153 0ustar srudsrudNaming/PredicateName: NameWhitelist: - is_enabled? Metrics/LineLength: Max: 120 Style/RedundantSelf: Enabled: false Style/PreferredHashMethods: Enabled: false Style/StringLiterals: Enabled: false Layout/SpaceBeforeBlockBraces: Enabled: false unleash-0.1.6/Rakefile0000644000175000017500000000016513570535542013351 0ustar srudsrudrequire "bundler/gem_tasks" require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) task :default => :spec unleash-0.1.6/bin/0000755000175000017500000000000013570535542012452 5ustar srudsrudunleash-0.1.6/bin/unleash-client0000755000175000017500000000541113570535542015314 0ustar srudsrud#!/usr/bin/env ruby require 'optparse' require 'unleash' require 'unleash/client' require 'unleash/context' options = { variant: false, verbose: false, quiet: false, url: 'http://localhost:4242', demo: false, disable_metrics: true, sleep: 0.1, } OptionParser.new do |opts| opts.banner = "Usage: #{__FILE__} [options] feature [key1=val1] [key2=val2]" opts.on("-V", "--variant", "Fetch variant for feature") do |v| options[:variant] = v end opts.on("-v", "--[no-]verbose", "Run verbosely") do |v| options[:verbose] = v end opts.on("-q", "--quiet", "Quiet mode, minimum output only") do |v| options[:quiet] = v end opts.on("-uURL", "--url=URL", "URL base for the Unleash feature toggle service") do |u| options[:url] = u end opts.on("-d", "--demo", "Demo load by looping, instead of a simple lookup") do |d| options[:demo] = d end opts.on("-m", "--[no-]metrics", "Enable metrics reporting") do |m| options[:disable_metrics] = !m end opts.on("-sSLEEP", "--sleep=SLEEP", Float, "Sleep interval between checks (seconds) in demo") do |s| options[:sleep] = s end opts.on("-h", "--help", "Prints this help") do puts opts exit end end.parse! feature_name = ARGV.shift raise 'feature_name is required. see --help for usage.' unless feature_name options[:verbose] = false if options[:quiet] @unleash = Unleash::Client.new( url: options[:url], app_name: 'unleash-client-ruby-cli', disable_metrics: options[:metrics], log_level: log_level = options[:quiet] ? Logger::ERROR : (options[:verbose] ? Logger::DEBUG : Logger::WARN), ) context_params = ARGV.map{ |e| e.split("=") }.map{ |k,v| [k.to_sym, v] }.to_h context_properties = context_params.reject{ |k,v| [:user_id, :session_id, :remote_address].include? k } context_params.select!{ |k,v| [:user_id, :session_id, :remote_address].include? k } context_params.merge!( properties: context_properties ) unless context_properties.nil? unleash_context = Unleash::Context.new(context_params) if options[:verbose] puts "Running configuration:" p options puts "feature: #{feature_name}" puts "context_args: #{ARGV}" puts "context_params: #{context_params}" puts "context: #{unleash_context}" puts "" end if options[:demo] loop do enabled = @unleash.is_enabled?(feature_name, unleash_context) print enabled ? '.' : '|' sleep options[:sleep] end else if options[:variant] variant = @unleash.get_variant(feature_name, unleash_context) puts " For feature \'#{feature_name}\' got variant \'#{variant}\'" else if @unleash.is_enabled?(feature_name, unleash_context) puts " \'#{feature_name}\' is enabled according to unleash" else puts " \'#{feature_name}\' is disabled according to unleash" end end end @unleash.shutdown unleash-0.1.6/bin/setup0000755000175000017500000000020313570535542013533 0ustar srudsrud#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here unleash-0.1.6/bin/console0000755000175000017500000000053513570535542014045 0ustar srudsrud#!/usr/bin/env ruby require "bundler/setup" require "unleash/client" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start require "irb" IRB.start(__FILE__) unleash-0.1.6/LICENSE0000644000175000017500000002611713570535542012716 0ustar srudsrud Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2018 Renato Arruda Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. unleash-0.1.6/README.md0000644000175000017500000002165013570535542013165 0ustar srudsrud# Unleash::Client [![Build Status](https://travis-ci.org/Unleash/unleash-client-ruby.svg?branch=master)](https://travis-ci.org/Unleash/unleash-client-ruby) [![Coverage Status](https://coveralls.io/repos/github/Unleash/unleash-client-ruby/badge.svg?branch=master)](https://coveralls.io/github/Unleash/unleash-client-ruby?branch=master) [![Gem Version](https://badge.fury.io/rb/unleash.svg)](https://badge.fury.io/rb/unleash) Unleash client so you can roll out your features with confidence. Leverage the [Unleash Server](https://github.com/Unleash/unleash) for powerful feature toggling in your ruby/rails applications. ## Supported Ruby Interpreters * MRI 2.3 * MRI 2.4 * MRI 2.5 * MRI 2.6 * jruby ## Installation Add this line to your application's Gemfile: ```ruby gem 'unleash', '~> 0.1.6' ``` And then execute: $ bundle Or install it yourself as: $ gem install unleash ## Configure It is **required** to configure the `url` of the unleash server and `app_name` with the name of the runninng application. Please substitute the sample `'http://unleash.herokuapp.com/api'` for the url of your own instance. It is **highly recommended** to configure the `instance_id` parameter as well. ```ruby Unleash.configure do |config| config.url = 'http://unleash.herokuapp.com/api' config.app_name = 'my_ruby_app' end ``` or instantiate the client with the valid configuration: ```ruby UNLEASH = Unleash::Client.new(url: 'http://unleash.herokuapp.com/api', app_name: 'my_ruby_app') ``` #### List of Arguments Argument | Description | Required? | Type | Default Value| ---------|-------------|-----------|-------|---------------| `url` | Unleash server URL. | Y | String | N/A | `app_name` | Name of your program. | Y | String | N/A | `instance_id` | Identifier for the running instance of program. Important so you can trace back to where metrics are being collected from. **Highly recommended be be set.** | N | String | random UUID | `environment` | Environment the program is running on. Could be for example `prod` or `dev`. Not yet in use. | N | String | `default` | `refresh_interval` | How often the unleash client should check with the server for configuration changes. | N | Integer | 15 | `metrics_interval` | How often the unleash client should send metrics to server. | N | Integer | 10 | `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` | `disable_metrics` | Disables sending metrics to Unleash server. | N | Boolean | `false` | `custom_http_headers` | Custom headers to send to Unleash. | N | Hash | {} | `timeout` | How long to wait for the connection to be established or wait in reading state (open_timeout/read_timeout) | N | Integer | 30 | `retry_limit` | How many consecutive failures in connecting to the Unleash server are allowed before giving up. | N | Integer | 1 | `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::ERROR` | For in a more in depth look, please see `lib/unleash/configuration.rb`. ## Usage in a plain Ruby Application ```ruby require 'unleash' require 'unleash/context' @unleash = Unleash::Client.new(url: 'http://unleash.herokuapp.com/api', app_name: 'my_ruby_app') feature_name = "AwesomeFeature" unleash_context = Unleash::Context.new unleash_context.user_id = 123 if @unleash.is_enabled?(feature_name, unleash_context) puts " #{feature_name} is enabled according to unleash" else puts " #{feature_name} is disabled according to unleash" end ``` ## Usage in a Rails Application #### Add Initializer Put in `config/initializers/unleash.rb`: ```ruby Unleash.configure do |config| config.url = 'http://unleash.herokuapp.com/api' config.app_name = Rails.application.class.parent.to_s # config.instance_id = "#{Socket.gethostname}" config.logger = Rails.logger config.environment = Rails.env end UNLEASH = Unleash::Client.new ``` 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. #### Add Initializer if using [Puma](https://github.com/puma/puma) In `puma.rb` ensure that the unleash client is configured and instantiated as below, inside the `on_worker_boot` code block: ```ruby on_worker_boot do # ... Unleash.configure do |config| config.url = 'http://unleash.herokuapp.com/api' config.app_name = Rails.application.class.parent.to_s config.environment = Rails.env end Rails.configuration.unleash = Unleash::Client.new end ``` Instead of the configuration in `config/initializers/unleash.rb`. #### Set Unleash::Context Be sure to add the following method and callback in the application controller to have `@unleash_context` set for all requests: Add in `app/controllers/application_controller.rb`: ```ruby before_action :set_unleash_context private def set_unleash_context @unleash_context = Unleash::Context.new( session_id: session.id, remote_address: request.remote_ip, user_id: session[:user_id] ) end ``` Or if you see better fit, only in the controllers that you will be using unleash. #### Sample usage Then wherever in your application that you need a feature toggle, you can use: ```ruby if UNLEASH.is_enabled? "AwesomeFeature", @unleash_context puts "AwesomeFeature is enabled" end ``` or if client is set in `Rails.configuration.unleash`: ```ruby if Rails.configuration.unleash.is_enabled? "AwesomeFeature", @unleash_context puts "AwesomeFeature is enabled" end ``` If the feature is not found in the server, it will by default return false. However you can override that by setting the default return value to `true`: ```ruby if UNLEASH.is_enabled? "AwesomeFeature", @unleash_context, true puts "AwesomeFeature is enabled by default" end ``` Alternatively by using `if_enabled` you can send a code block to be executed as a parameter: ```ruby UNLEASH.if_enabled "AwesomeFeature", @unleash_context, true do puts "AwesomeFeature is enabled by default" end ``` ##### Variations If no variant is found in the server, use the fallback variant. ```ruby fallback_variant = Unleash::Variant.new(name: 'default', enabled: true, payload: {"color" => "blue"}) variant = UNLEASH.get_variant "ColorVariants", @unleash_context, fallback_variant puts "variant color is: #{variant.payload.fetch('color')}" ``` #### Client methods Method Name | Description | Return Type | ---------|-------------|-------------| `is_enabled?` | Check if feature toggle is to be enabled or not. | Boolean | `enabled?` | Alias to the `is_enabled?` method. But more ruby idiomatic. | Boolean | `if_enabled` | Run a code block, if a feature is enabled. | `yield` | `get_variant` | Get variant for a given feature | `Unleash::Variant` | `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 | `shutdown!` | Kill ToggleFetcher and MetricsReporter threads immediately. | nil | ## Local test client ``` # cli unleash client: bundle exec bin/unleash-client --help # or a simple sample implementation (with values hardcoded): bundle exec examples/simple.rb ``` ## Available Strategies This client comes with the all the required strategies out of the box: * ApplicationHostnameStrategy * DefaultStrategy * GradualRolloutRandomStrategy * GradualRolloutSessionIdStrategy * GradualRolloutUserIdStrategy * RemoteAddressStrategy * UnknownStrategy * UserWithIdStrategy ## Development 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. 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). See [TODO.md](TODO.md) for known limitations, and feature roadmap. ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/unleash/unleash-client-ruby. Please include tests with any pull requests, to avoid regressions. unleash-0.1.6/.gitignore0000644000175000017500000000027213570535542013673 0ustar srudsrud/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ # rspec failure tracking .rspec_status # Clone of the client-specification /client-specification/unleash-0.1.6/.travis.yml0000644000175000017500000000041613570535542014014 0ustar srudsrudsudo: false language: ruby rvm: - 2.3 - 2.4 - 2.5 - 2.6 - jruby before_install: - gem install bundler -v 2.0.2 # install client spec from official repository: - git clone --depth 5 https://github.com/Unleash/client-specification.git client-specification unleash-0.1.6/TODO.md0000644000175000017500000000260513570535542012774 0ustar srudsrudTODO ==== The Ruby client should be pretty stable now. But no warranty is given, and some work still remains. Implement: ---------- * Document writing of custom strategies. To test: (and write tests for) -------- * MetricsReporter * everything else :) To consider: ------------ * Not using class hierarchy for strategies (more duck typing) * Better compliance to https://github.com/rubocop-hq/ruby-style-guide * Reduce the amount of comments and logs DONE: ----- * Client registration with the server. * Reporter of the status of the feature toggles used. * Abstract the Thread/sleep loop with scheduled_executor * Thread the Reporter code * Tests for All of strategies * Configure via yield/blk * Configurable Logging (logger + level) * Switch hashing function to use murmurhash3 as per https://github.com/Unleash/unleash/issues/247 * Document usage with Rails * Support for allowing custom strategies. * Possibility for custom HTTP headers when interacting with unleash server. * Implement spec test for ruby client specs using https://github.com/Unleash/client-specification/blob/master/05-gradual-rollout-random-strategy.json (similar to the examples below) to ensure consistent client behaviour. * java: https://github.com/Unleash/unleash-client-java/compare/master...integration-spec * node: https://github.com/Unleash/unleash-client-node/compare/client-specification?expand=1 unleash-0.1.6/unleash-client.gemspec0000644000175000017500000000254413570535542016167 0ustar srudsrud# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'unleash/version' Gem::Specification.new do |spec| spec.name = "unleash" spec.version = Unleash::VERSION spec.authors = ["Renato Arruda"] spec.email = ["rarruda@rarruda.org"] spec.licenses = ["Apache-2.0"] spec.summary = %q{Unleash feature toggle client.} spec.description = %q{This is the ruby client for Unleash, a powerful feature toggle system that gives you a great overview over all feature toggles across all your applications and services.} spec.homepage = "https://github.com/unleash/unleash-client-ruby" spec.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r{^(test|spec|features)/}) end spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.required_ruby_version = "~> 2.2" spec.add_dependency "murmurhash3", "~> 0.1.6" spec.add_development_dependency "bundler", "~> 2.0" spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "rspec", "~> 3.0" spec.add_development_dependency "rspec-json_expectations", "~> 2.1" spec.add_development_dependency "webmock", "~> 3.0" spec.add_development_dependency "coveralls" end unleash-0.1.6/lib/0000755000175000017500000000000013570535542012450 5ustar srudsrudunleash-0.1.6/lib/unleash.rb0000644000175000017500000000203013570535542014427 0ustar srudsrudrequire 'unleash/version' require 'unleash/configuration' require 'unleash/strategy/base' require 'unleash/context' require 'unleash/client' require 'logger' Gem.find_files('unleash/strategy/**/*.rb').each { |path| require path } module Unleash TIME_RESOLUTION = 3 STRATEGIES = Unleash::Strategy.constants .select { |c| Unleash::Strategy.const_get(c).is_a? Class } .select { |c| !['NotImplemented', 'Base'].include?(c.to_s) } .map { |c| lowered_c = c.to_s lowered_c[0] = lowered_c[0].downcase [lowered_c.to_sym, Object::const_get("Unleash::Strategy::#{c}").new] } .to_h class << self attr_accessor :configuration, :toggle_fetcher, :toggles, :toggle_metrics, :reporter, :logger end def self.initialize self.toggles = [] self.toggle_metrics = {} end # Support for configuration via yield: def self.configure() self.configuration ||= Unleash::Configuration.new yield(configuration) self.configuration.validate! self.configuration.refresh_backup_file! end end unleash-0.1.6/lib/unleash/0000755000175000017500000000000013570535542014107 5ustar srudsrudunleash-0.1.6/lib/unleash/metrics_reporter.rb0000755000175000017500000000342013570535542020026 0ustar srudsrudrequire 'unleash/configuration' require 'unleash/metrics' require 'net/http' require 'json' require 'time' module Unleash class MetricsReporter attr_accessor :last_time def initialize self.last_time = Time.now end def generate_report now = Time.now start, stop, self.last_time = self.last_time, now, now report = { 'appName': Unleash.configuration.app_name, 'instanceId': Unleash.configuration.instance_id, 'bucket': { 'start': start.iso8601(Unleash::TIME_RESOLUTION), 'stop': stop.iso8601(Unleash::TIME_RESOLUTION), 'toggles': Unleash.toggle_metrics.features } } Unleash.toggle_metrics.reset return report end def send Unleash.logger.debug "send() Report" generated_report = self.generate_report() uri = URI(Unleash.configuration.client_metrics_url) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true if uri.scheme == 'https' http.open_timeout = Unleash.configuration.timeout # in seconds http.read_timeout = Unleash.configuration.timeout # in seconds headers = (Unleash.configuration.get_http_headers || {}).dup headers['Content-Type'] = 'application/json' request = Net::HTTP::Post.new(uri.request_uri, headers) request.body = generated_report.to_json Unleash.logger.debug "Report to send: #{request.body}" response = http.request(request) if ['200','202'].include? response.code Unleash.logger.debug "Report sent to unleash server sucessfully. Server responded with http code #{response.code}" else Unleash.logger.error "Error when sending report to unleash server. Server responded with http code #{response.code}." end end end end unleash-0.1.6/lib/unleash/toggle_fetcher.rb0000755000175000017500000001126013570535542017420 0ustar srudsrudrequire 'unleash/configuration' require 'net/http' require 'json' require 'thread' module Unleash class ToggleFetcher 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 # start by fetching synchronously, and failing back to reading the backup file. begin fetch rescue Exception => e 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 # once initialized, somewhere else you will want to start a loop with fetch() end def toggles self.toggle_lock.synchronize do # wait for resource, only if it is null self.toggle_resource.wait(self.toggle_lock) if self.toggle_cache.nil? return self.toggle_cache end end # rename to refresh_from_server! ?? # TODO: should simplify by moving uri / http initialization elsewhere def fetch Unleash.logger.debug "fetch()" Unleash.logger.debug "ETag: #{self.etag}" unless self.etag.nil? uri = URI(Unleash.configuration.fetch_toggles_url) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true if uri.scheme == 'https' http.open_timeout = Unleash.configuration.timeout # in seconds http.read_timeout = Unleash.configuration.timeout # in seconds headers = (Unleash.configuration.get_http_headers || {}).dup headers['Content-Type'] = 'application/json' headers['If-None-Match'] = self.etag unless self.etag.nil? request = Net::HTTP::Get.new(uri.request_uri, headers) response = http.request(request) Unleash.logger.debug "No changes according to the unleash server, nothing to do." if response.code == '304' return if response.code == '304' raise IOError, "Unleash server returned a non 200/304 HTTP result." if response.code != '200' 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 # always synchronize with the local cache when fetching: synchronize_with_local_cache!(features) Unleash.logger.info "Flush changes to running client variable" update_client! Unleash.logger.info "Saved to toggle cache, will save to disk now" save! end def save! begin backup_file = Unleash.configuration.backup_file backup_file_tmp = "#{backup_file}.tmp" self.toggle_lock.synchronize do file = File.open(backup_file_tmp, "w") file.write(self.toggle_cache.to_json) File.rename(backup_file_tmp, backup_file) end rescue Exception => e # This is not really the end of the world. Swallowing the exception. Unleash.logger.error "Unable to save backup file. Exception thrown #{e.class}:'#{e}'" Unleash.logger.error "stacktrace: #{e.backtrace}" ensure file.close if defined?(file) && ! file.nil? self.toggle_lock.unlock if self.toggle_lock.locked? end end private def synchronize_with_local_cache!(features) if self.toggle_cache != features self.toggle_lock.synchronize do self.toggle_cache = features end # notify all threads waiting for this resource to no longer wait self.toggle_resource.broadcast end end def update_client! if Unleash.toggles != self.toggles Unleash.logger.info "Updating toggles to main client, there has been a change in the server." Unleash.toggles = self.toggles end end def read! Unleash.logger.debug "read!()" return nil unless File.exist?(Unleash.configuration.backup_file) begin file = File.new(Unleash.configuration.backup_file, "r") file_content = file.read backup_as_hash = JSON.parse(file_content) synchronize_with_local_cache!(backup_as_hash) update_client! rescue IOError => e Unleash.logger.error "Unable to read the backup_file." rescue JSON::ParserError => e Unleash.logger.error "Unable to parse JSON from existing backup_file." rescue Exception => e Unleash.logger.error "Unable to extract valid data from backup_file. Exception thrown", e ensure file.close unless file.nil? end end end end unleash-0.1.6/lib/unleash/metrics.rb0000644000175000017500000000163113570535542016103 0ustar srudsrudmodule Unleash class Metrics attr_accessor :features # NOTE: no mutexes for features def initialize self.features = {} end def to_s self.features.to_json end def increment(feature, choice) raise "InvalidArgument choice must be :yes or :no" unless [:yes, :no].include? choice self.features[feature] = {yes: 0, no: 0} unless self.features.include? feature self.features[feature][choice] += 1 end def increment_variant(feature, variant) self.features[feature] = {yes: 0, no: 0} unless self.features.include? feature self.features[feature]['variant'] = {} unless self.features[feature].include? 'variant' self.features[feature]['variant'][variant] = 0 unless self.features[feature]['variant'].include? variant self.features[feature]['variant'][variant] += 1 end def reset self.features = {} end end end unleash-0.1.6/lib/unleash/strategy/0000755000175000017500000000000013570535542015751 5ustar srudsrudunleash-0.1.6/lib/unleash/strategy/remote_address.rb0000644000175000017500000000105113570535542021273 0ustar srudsrudmodule Unleash module Strategy class RemoteAddress < Base PARAM = 'IPs' def name 'remoteAddress' end # need: params['IPs'], context.remote_address def is_enabled?(params = {}, context = nil) return false unless params.is_a?(Hash) && params.has_key?(PARAM) return false unless params.fetch(PARAM, nil).is_a? String return false unless context.class.name == 'Unleash::Context' params[PARAM].split(',').map(&:strip).include?(context.remote_address) end end end end unleash-0.1.6/lib/unleash/strategy/gradual_rollout_random.rb0000644000175000017500000000103613570535542023035 0ustar srudsrudrequire 'unleash/strategy/util' module Unleash module Strategy class GradualRolloutRandom < Base def name 'gradualRolloutRandom' end # need: params['percentage'] def is_enabled?(params = {}, _context = nil) return false unless params.is_a?(Hash) && params.has_key?('percentage') begin percentage = Integer(params['percentage'] || 0) rescue ArgumentError => e return false end (percentage >= Random.rand(1..100)) end end end end unleash-0.1.6/lib/unleash/strategy/base.rb0000644000175000017500000000047613570535542017217 0ustar srudsrudmodule Unleash module Strategy class NotImplemented < Exception end class Base def name raise NotImplemented, "Strategy is not implemented" end def is_enabled?(params = {}, context = nil) raise NotImplemented, "Strategy is not implemented" end end end end unleash-0.1.6/lib/unleash/strategy/gradual_rollout_userid.rb0000644000175000017500000000123513570535542023051 0ustar srudsrudrequire 'unleash/strategy/util' module Unleash module Strategy class GradualRolloutUserId < Base def name 'gradualRolloutUserId' end # need: params['percentage'], params['groupId'], context.user_id, def is_enabled?(params = {}, context) return false unless params.is_a?(Hash) && params.has_key?('percentage') return false unless context.class.name == 'Unleash::Context' return false if context.user_id.empty? percentage = Integer(params['percentage'] || 0) (percentage > 0 && Util.get_normalized_number(context.user_id, params['groupId'] || "") <= percentage) end end end end unleash-0.1.6/lib/unleash/strategy/gradual_rollout_sessionid.rb0000644000175000017500000000125113570535542023554 0ustar srudsrudrequire 'unleash/strategy/util' module Unleash module Strategy class GradualRolloutSessionId < Base def name 'gradualRolloutSessionId' end # need: params['percentage'], params['groupId'], context.user_id, def is_enabled?(params = {}, context) return false unless params.is_a?(Hash) && params.has_key?('percentage') return false unless context.class.name == 'Unleash::Context' return false if context.session_id.empty? percentage = Integer(params['percentage'] || 0) (percentage > 0 && Util.get_normalized_number(context.session_id, params['groupId'] || "") <= percentage) end end end end unleash-0.1.6/lib/unleash/strategy/user_with_id.rb0000644000175000017500000000104213570535542020760 0ustar srudsrudmodule Unleash module Strategy class UserWithId < Base PARAM = 'userIds' def name 'userWithId' end # requires: params['userIds'], context.user_id, def is_enabled?(params = {}, context = nil) return false unless params.is_a?(Hash) && params.has_key?(PARAM) return false unless params.fetch(PARAM, nil).is_a? String return false unless context.class.name == 'Unleash::Context' params[PARAM].split(",").map(&:strip).include?(context.user_id) end end end end unleash-0.1.6/lib/unleash/strategy/application_hostname.rb0000644000175000017500000000107313570535542022500 0ustar srudsrudrequire 'socket' module Unleash module Strategy class ApplicationHostname < Base attr_accessor :hostname PARAM = 'hostnames' def initialize self.hostname = Socket.gethostname || 'undefined' end def name 'applicationHostname' end # need: :params['hostnames'] def is_enabled?(params = {}, _context = nil) return false unless params.is_a?(Hash) && params.has_key?(PARAM) params[PARAM].split(",").map(&:strip).map{ |h| h.downcase }.include?(self.hostname) end end end end unleash-0.1.6/lib/unleash/strategy/util.rb0000644000175000017500000000057313570535542017260 0ustar srudsrudrequire 'murmurhash3' module Unleash module Strategy module Util module_function NORMALIZER = 100 # convert the two strings () into a number between 1 and base (100 by default) def get_normalized_number(identifier, group_id, base = NORMALIZER) MurmurHash3::V32.str_hash("#{group_id}:#{identifier}") % base + 1 end end end end unleash-0.1.6/lib/unleash/strategy/default.rb0000644000175000017500000000030113570535542017714 0ustar srudsrudmodule Unleash module Strategy class Default < Base def name 'default' end def is_enabled?(params = {}, context = nil) true end end end end unleash-0.1.6/lib/unleash/configuration.rb0000644000175000017500000000523513570535542017310 0ustar srudsrudrequire 'securerandom' require 'tmpdir' module Unleash class Configuration attr_accessor :url, :app_name, :environment, :instance_id, :custom_http_headers, :disable_client, :disable_metrics, :timeout, :retry_limit, :refresh_interval, :metrics_interval, :backup_file, :logger, :log_level def initialize(opts = {}) self.app_name = opts[:app_name] || nil self.environment = opts[:environment] || 'default' self.url = opts[:url] || nil self.instance_id = opts[:instance_id] || SecureRandom.uuid if opts[:custom_http_headers].is_a?(Hash) || opts[:custom_http_headers].nil? self.custom_http_headers = opts[:custom_http_headers] || {} else raise ArgumentError, "custom_http_headers must be a hash." end self.disable_client = opts[:disable_client] || false self.disable_metrics = opts[:disable_metrics] || false self.refresh_interval = opts[:refresh_interval] || 15 self.metrics_interval = opts[:metrics_interval] || 10 self.timeout = opts[:timeout] || 30 self.retry_limit = opts[:retry_limit] || 1 self.backup_file = opts[:backup_file] || nil self.logger = opts[:logger] || Logger.new(STDOUT) self.log_level = opts[:log_level] || Logger::WARN if opts[:logger].nil? # on default logger, use custom formatter that includes thread_name: self.logger.formatter = proc do |severity, datetime, progname, msg| thread_name = (Thread.current[:name] || "Unleash").rjust(16, ' ') "[#{datetime.iso8601(6)} #{thread_name} #{severity.ljust(5, ' ')}] : #{msg}\n" end end refresh_backup_file! end def metrics_interval_in_millis self.metrics_interval * 1_000 end def validate! return if self.disable_client raise ArgumentError, "URL and app_name are required parameters." if self.app_name.nil? or self.url.nil? raise ArgumentError, "custom_http_headers must be a hash." unless self.custom_http_headers.is_a?(Hash) end def refresh_backup_file! if self.backup_file.nil? self.backup_file = Dir.tmpdir + "/unleash-#{app_name}-repo.json" end end def get_http_headers { 'UNLEASH-INSTANCEID' => self.instance_id, 'UNLEASH-APPNAME' => self.app_name }.merge(custom_http_headers.dup) end def fetch_toggles_url self.url + '/client/features' end def client_metrics_url self.url + '/client/metrics' end def client_register_url self.url + '/client/register' end end end unleash-0.1.6/lib/unleash/feature_toggle.rb0000644000175000017500000001120313570535542017425 0ustar srudsrudrequire 'unleash/activation_strategy' require 'unleash/variant_definition' require 'unleash/variant' require 'unleash/strategy/util' require 'securerandom' module Unleash class FeatureToggle attr_accessor :name, :enabled, :strategies, :variant_definitions def initialize(params={}) params = {} if params.nil? self.name = params.fetch('name', nil) self.enabled = params.fetch('enabled', false) self.strategies = params.fetch('strategies', []) .select{ |s| ( s.key?('name') && Unleash::STRATEGIES.key?(s['name'].to_sym) ) } .map{ |s| ActivationStrategy.new(s['name'], s['parameters'] || {}) } || [] self.variant_definitions = (params.fetch('variants', []) || []) .select{ |v| v.is_a?(Hash) && v.key?('name') } .map{ |v| VariantDefinition.new( v.fetch('name', ''), v.fetch('weight', 0), v.fetch('payload', nil), v.fetch('overrides', []) ) } || [] end def to_s "" end def is_enabled?(context, default_result) unless ['NilClass', 'Unleash::Context'].include? context.class.name Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, please use Unleash::Context. Context set to nil." context = nil end result = am_enabled?(context, default_result) choice = result ? :yes : :no Unleash.toggle_metrics.increment(name, choice) unless Unleash.configuration.disable_metrics return result end def get_variant(context, fallback_variant = disabled_variant) unless ['NilClass', 'Unleash::Context'].include? context.class.name Unleash.logger.error "Provided context is not of the correct type #{context.class.name}, please use Unleash::Context. Context set to nil." context = nil end unless ['Unleash::Variant'].include? fallback_variant.class.name raise ArgumentError, "Provided fallback_variant is not of the correct type #{fallback_variant.class.name}, please use Unleash::Variant." end return disabled_variant unless self.enabled && am_enabled?(context, true) return disabled_variant if get_sum_variant_defs_weights <= 0 variant = variant_from_override_match(context) variant = variant_from_weights(context) if variant.nil? Unleash.toggle_metrics.increment_variant(self.name, variant.name) unless Unleash.configuration.disable_metrics return variant end private # only check if it is enabled, do not do metrics def am_enabled?(context, default_result) result = if self.enabled self.strategies.empty? || self.strategies.select{ |s| strategy = Unleash::STRATEGIES.fetch(s.name.to_sym, :unknown) r = strategy.is_enabled?(s.params, context) Unleash.logger.debug "Strategy #{s.name} returned #{r} with context: #{context}" #"for params #{s.params} " r }.any? else default_result end Unleash.logger.debug "FeatureToggle (enabled:#{self.enabled} default_result:#{default_result} and Strategies combined returned #{result})" return result end def disabled_variant Unleash::Variant.new(name: 'disabled', enabled: false) end def get_sum_variant_defs_weights self.variant_definitions.map{ |v| v.weight }.reduce(0, :+) end def variant_salt(context) return context.user_id unless context.user_id.to_s.empty? return context.session_id unless context.session_id.to_s.empty? return context.remote_address unless context.remote_address.to_s.empty? return SecureRandom.random_number end def variant_from_override_match(context) variant = self.variant_definitions.select{ |vd| vd.override_matches_context?(context) }.first return nil if variant.nil? Unleash::Variant.new(name: variant.name, enabled: true, payload: variant.payload) end def variant_from_weights(context) variant_weight = Unleash::Strategy::Util.get_normalized_number(variant_salt(context), self.name, get_sum_variant_defs_weights()) prev_weights = 0 variant_definition = self.variant_definitions .select{ |v| res = (prev_weights + v.weight >= variant_weight) prev_weights += v.weight res } .first return disabled_variant if variant_definition.nil? Unleash::Variant.new(name: variant_definition.name, enabled: true, payload: variant_definition.payload) end end end unleash-0.1.6/lib/unleash/variant_override.rb0000644000175000017500000000234713570535542020005 0ustar srudsrudmodule Unleash class VariantOverride attr_accessor :context_name, :values def initialize(context_name, values = []) self.context_name = context_name self.values = values || [] validate end def to_s "" end def matches_context?(context) raise ArgumentError, 'context must be of class Unleash::Context' unless context.class.name == 'Unleash::Context' context_value = case self.context_name when 'userId' context.user_id when 'sessionId' context.session_id when 'remoteAddress' context.remote_address else context.properties.fetch(self.context_name, nil) end Unleash.logger.debug "VariantOverride: context_name: #{context_name} context_value: #{context_value}" self.values.include? context_value.to_s end private def validate raise ArgumentError, 'context_name must be a String' unless self.context_name.is_a?(String) raise ArgumentError, 'values must be an Array of strings' unless self.values.is_a?(Array) \ && self.values.reject{ |v| v.is_a?(String) }.empty? end end end unleash-0.1.6/lib/unleash/client.rb0000644000175000017500000001276613570535542015726 0ustar srudsrudrequire 'unleash/configuration' require 'unleash/toggle_fetcher' require 'unleash/metrics_reporter' require 'unleash/scheduled_executor' require 'unleash/feature_toggle' require 'logger' require 'time' module Unleash class Client attr_accessor :fetcher_scheduled_executor, :metrics_scheduled_executor def initialize(*opts) Unleash.configuration ||= Unleash::Configuration.new(*opts) Unleash.configuration.validate! Unleash.logger = Unleash.configuration.logger.clone Unleash.logger.level = Unleash.configuration.log_level unless Unleash.configuration.disable_client Unleash.toggle_fetcher = Unleash::ToggleFetcher.new register self.fetcher_scheduled_executor = Unleash::ScheduledExecutor.new('ToggleFetcher', Unleash.configuration.refresh_interval) self.fetcher_scheduled_executor.run do Unleash.toggle_fetcher.fetch end unless Unleash.configuration.disable_metrics Unleash.toggle_metrics = Unleash::Metrics.new Unleash.reporter = Unleash::MetricsReporter.new self.metrics_scheduled_executor = Unleash::ScheduledExecutor.new('MetricsReporter', Unleash.configuration.metrics_interval) self.metrics_scheduled_executor.run do Unleash.reporter.send end end else Unleash.logger.warn "Unleash::Client is disabled! Will only return default results!" end end def is_enabled?(feature, context = nil, default_value = false) Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} with context #{context}" 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 Unleash.toggles if toggle_as_hash.nil? Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} not found" return default_value end toggle = Unleash::FeatureToggle.new(toggle_as_hash) toggle_result = toggle.is_enabled?(context, default_value) return toggle_result end # enabled? is a more ruby idiomatic method name than is_enabled? alias_method :enabled?, :is_enabled? # execute a code block (passed as a parameter), if is_enabled? is true. def if_enabled(feature, context = nil, default_value = false, &blk) yield if is_enabled?(feature, context, default_value) end def get_variant(feature, context = nil, fallback_variant = false) Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}" if Unleash.configuration.disable_client Unleash.logger.warn "unleash_client is disabled! Always returning #{default_variant} for feature #{feature}!" return fallback_variant || Unleash::FeatureToggle.disabled_variant end toggle_as_hash = Unleash.toggles.select{ |toggle| toggle['name'] == feature }.first if Unleash.toggles if toggle_as_hash.nil? Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} not found" return fallback_variant || Unleash::FeatureToggle.disabled_variant end toggle = Unleash::FeatureToggle.new(toggle_as_hash) variant = toggle.get_variant(context, fallback_variant) if variant.nil? Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found" return fallback_variant || Unleash::FeatureToggle.disabled_variant end # TODO: Add to README: name, payload, enabled (bool) return variant end # safe shutdown: also flush metrics to server and toggles to disk def shutdown unless Unleash.configuration.disable_client Unleash.toggle_fetcher.save! Unleash.reporter.send unless Unleash.configuration.disable_metrics shutdown! end end # quick shutdown: just kill running threads def shutdown! unless Unleash.configuration.disable_client self.fetcher_scheduled_executor.exit self.metrics_scheduled_executor.exit unless Unleash.configuration.disable_metrics end end private def info return { 'appName': Unleash.configuration.app_name, 'instanceId': Unleash.configuration.instance_id, 'sdkVersion': "unleash-client-ruby:" + Unleash::VERSION, 'strategies': Unleash::STRATEGIES.keys, 'started': Time.now.iso8601(Unleash::TIME_RESOLUTION), 'interval': Unleash.configuration.metrics_interval_in_millis } end def register Unleash.logger.debug "register()" uri = URI(Unleash.configuration.client_register_url) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true if uri.scheme == 'https' http.open_timeout = Unleash.configuration.timeout # in seconds http.read_timeout = Unleash.configuration.timeout # in seconds headers = (Unleash.configuration.get_http_headers || {}).dup headers['Content-Type'] = 'application/json' request = Net::HTTP::Post.new(uri.request_uri, headers) request.body = info.to_json # Send the request, if possible begin response = http.request(request) rescue Exception => e Unleash.logger.error "unable to register client with unleash server due to exception #{e.class}:'#{e}'." Unleash.logger.error "stacktrace: #{e.backtrace}" end end end end unleash-0.1.6/lib/unleash/activation_strategy.rb0000644000175000017500000000052513570535542020521 0ustar srudsrudmodule Unleash class ActivationStrategy attr_accessor :name, :params def initialize(name, params) self.name = name if params.is_a?(Hash) self.params = params else Unleash.logger.warn "Invalid params provided for ActivationStrategy #{params}" self.params = {} end end end end unleash-0.1.6/lib/unleash/variant.rb0000644000175000017500000000133013570535542016075 0ustar srudsrud module Unleash class Variant attr_accessor :name, :enabled, :payload def initialize(params = {}) raise ArgumentError, "Variant initializer requires a hash." unless params.is_a?(Hash) self.name = params.values_at('name', :name).compact.first self.enabled = params.values_at('enabled', :enabled).compact.first || false self.payload = params.values_at('payload', :payload).compact.first raise ArgumentError, "Variant requires a name." if self.name.nil? end def to_s "" end def ==(v) self.name == v.name && self.enabled == v.enabled && self.payload == v.payload end end end unleash-0.1.6/lib/unleash/variant_definition.rb0000644000175000017500000000143613570535542020314 0ustar srudsrudrequire 'unleash/variant_override' module Unleash class VariantDefinition attr_accessor :name, :weight, :payload, :overrides def initialize(name, weight = 0, payload = nil, overrides = []) self.name = name self.weight = weight self.payload = payload # self.overrides = overrides self.overrides = (overrides || []) .select{ |v| v.is_a?(Hash) && v.key?('contextName') } .map{ |v| VariantOverride.new(v.fetch('contextName', ''), v.fetch('values', [])) } || [] end def override_matches_context?(context) self.overrides.select{ |o| o.matches_context?(context) }.first end def to_s "" end end end unleash-0.1.6/lib/unleash/scheduled_executor.rb0000755000175000017500000000301013570535542020307 0ustar srudsrudmodule Unleash class ScheduledExecutor attr_accessor :name, :interval, :max_exceptions, :retry_count, :thread def initialize(name, interval, max_exceptions = 5) self.name = name || '' self.interval = interval self.max_exceptions = max_exceptions self.retry_count = 0 self.thread = nil end def run(&blk) self.thread = Thread.new do Thread.current[:name] = self.name loop do Unleash.logger.debug "thread #{name} sleeping for #{interval} seconds" sleep interval Unleash.logger.debug "thread #{name} started" begin yield self.retry_count = 0 rescue Exception => e self.retry_count += 1 Unleash.logger.error "thread #{name} threw exception #{e.class}:'#{e}' (#{self.retry_count}/#{self.max_exceptions})" Unleash.logger.error "stacktrace: #{e.backtrace}" end if self.retry_count > self.max_exceptions Unleash.logger.error "thread #{name} retry_count (#{self.retry_count}) exceeded max_exceptions (#{self.max_exceptions}). Stopping with retries." break end end Unleash.logger.warn "thread #{name} loop ended" end end def running? self.thread.is_a?(Thread) && self.thread.alive? end def exit if self.running? Unleash.logger.warn "thread #{name} will exit!" self.thread.exit self.thread.join if self.running? end end end end unleash-0.1.6/lib/unleash/context.rb0000644000175000017500000000223113570535542016116 0ustar srudsrudmodule Unleash class Context attr_accessor :app_name, :environment, :user_id, :session_id, :remote_address, :properties def initialize(params = {}) raise ArgumentError, "Unleash::Context must be initialized with a hash." unless params.is_a?(Hash) self.app_name = params.values_at('appName', :app_name).compact.first || ( !Unleash.configuration.nil? ? Unleash.configuration.app_name : nil ) self.environment = params.values_at('environment', :environment).compact.first || ( !Unleash.configuration.nil? ? Unleash.configuration.environment : 'default' ) self.user_id = params.values_at('userId', :user_id).compact.first || '' self.session_id = params.values_at('sessionId', :session_id).compact.first || '' self.remote_address = params.values_at('remoteAddress', :remote_address).compact.first || '' properties = params.values_at('properties', :properties).compact.first self.properties = properties.is_a?(Hash) ? properties : {} end def to_s "" end end end unleash-0.1.6/lib/unleash/version.rb0000644000175000017500000000004713570535542016122 0ustar srudsrudmodule Unleash VERSION = "0.1.6" end unleash-0.1.6/examples/0000755000175000017500000000000013570535542013520 5ustar srudsrudunleash-0.1.6/examples/simple.rb0000755000175000017500000000224513570535542015344 0ustar srudsrud#!/usr/bin/env ruby require 'unleash' require 'unleash/context' puts ">> START simple.rb" # Unleash.configure do |config| # config.url = 'http://unleash.herokuapp.com/api' # config.app_name = 'simple-test' # config.refresh_interval = 2 # config.metrics_interval = 2 # config.retry_limit = 2 # end # @unleash = Unleash::Client.new # or: @unleash = Unleash::Client.new( url: 'http://unleash.herokuapp.com/api', app_name: 'simple-test', instance_id: 'local-test-cli', refresh_interval: 2, metrics_interval: 2, retry_limit: 2, log_level: Logger::DEBUG, ) # feature_name = "AwesomeFeature" feature_name = "4343443" 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 simple.rb" unleash-0.1.6/Gemfile0000644000175000017500000000014313570535542013173 0ustar srudsrudsource 'https://rubygems.org' # Specify your gem's dependencies in unleash-client.gemspec gemspec unleash-0.1.6/.rspec0000644000175000017500000000003713570535542013017 0ustar srudsrud--format documentation --color