Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
658c4ec
Work in progress on ruby v2 refactoring
jkodumal Jun 30, 2016
66d9378
Work in progress on v1 refactoring
jkodumal Jul 6, 2016
960f08a
Fix syntax error
jkodumal Jul 6, 2016
aeabe5f
Fix polling processor ctor
jkodumal Jul 6, 2016
18e7d04
Update imports, add create_worker to EventProcessor, update toggle an…
jkodumal Jul 6, 2016
1001299
Assign res in Event processor
jkodumal Jul 6, 2016
c2df040
Wait for initialization
jkodumal Jul 6, 2016
938573c
Fix syntax error in initializer
jkodumal Jul 6, 2016
e18e27c
Use correct floating point literal syntax
jkodumal Jul 6, 2016
aae8176
offline -> offline?
jkodumal Jul 6, 2016
e9d22da
Fix typo in stream initialization
jkodumal Jul 6, 2016
87a3302
Rescue and log timeout exception
jkodumal Jul 6, 2016
48a5609
Improve debug output in stream
jkodumal Jul 6, 2016
a373bc0
Add debug logging for event enqueuing
jkodumal Jul 6, 2016
2e3e6d9
Debug output for evaluation
jkodumal Jul 6, 2016
e61a05e
More debug output from streaming connection
jkodumal Jul 6, 2016
e4d0a48
Debug output for polling update processor
jkodumal Jul 6, 2016
99645f0
Fix typo in polling processor initialization
jkodumal Jul 6, 2016
771c42e
Import thread package
jkodumal Jul 6, 2016
8b5bc52
More debug output for polling
jkodumal Jul 6, 2016
c50b832
Debug output for requestor
jkodumal Jul 6, 2016
a8da405
Debug the sleep time in polling processor
jkodumal Jul 6, 2016
2264d0c
Clean up feature store definition
jkodumal Jul 6, 2016
1106ff3
Improve debug output
jkodumal Jul 6, 2016
79bd925
More debug output changes
jkodumal Jul 6, 2016
314c674
Remove more debugging
jkodumal Jul 6, 2016
c903e66
Fix endpoint for v1
jkodumal Jul 6, 2016
13f041e
Move evaluation methods into a separate sub-module
jkodumal Jul 6, 2016
dbad74f
Move evaluation sub-module
jkodumal Jul 6, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ldclient-rb.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ Gem::Specification.new do |spec|
spec.add_runtime_dependency "net-http-persistent", "~> 2.9"
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0.0"
spec.add_runtime_dependency "hashdiff", "~> 0.2"
spec.add_runtime_dependency "ld-celluloid-eventsource", "~> 0.4"
spec.add_runtime_dependency "ld-celluloid-eventsource", "~> 0.4"
spec.add_runtime_dependency "waitutil", "0.2"
end
7 changes: 6 additions & 1 deletion lib/ldclient-rb.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
require "ldclient-rb/version"
require "ldclient-rb/settings"
require "ldclient-rb/evaluation"
require "ldclient-rb/ldclient"
require "ldclient-rb/store"
require "ldclient-rb/cache_store"
require "ldclient-rb/config"
require "ldclient-rb/newrelic"
require "ldclient-rb/stream"
require "ldclient-rb/polling"
require "ldclient-rb/events"
require "ldclient-rb/feature_store"
require "ldclient-rb/requestor"
File renamed without changes.
55 changes: 26 additions & 29 deletions lib/ldclient-rb/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@ class Config
# connections in seconds.
# @option opts [Float] :connect_timeout (2) The connect timeout for network
# connections in seconds.
# @option opts [Object] :store A cache store for the Faraday HTTP caching
# @option opts [Object] :cache_store A cache store for the Faraday HTTP caching
# library. Defaults to the Rails cache in a Rails environment, or a
# thread-safe in-memory store otherwise.
# @option opts [Boolean] :offline (false) Whether the client should be initialized in
# offline mode. In offline mode, default values are returned for all flags and no
# remote network requests are made.
# @option opts [Float] :poll_interval (30) The number of seconds between polls for flag updates
# if streaming is off.
#
# @return [type] [description]
def initialize(opts = {})
Expand All @@ -42,14 +47,14 @@ def initialize(opts = {})
@events_uri = (opts[:events_uri] || Config.default_events_uri).chomp("/")
@capacity = opts[:capacity] || Config.default_capacity
@logger = opts[:logger] || Config.default_logger
@store = opts[:store] || Config.default_store
@cache_store = opts[:cache_store] || Config.default_cache_store
@flush_interval = opts[:flush_interval] || Config.default_flush_interval
@connect_timeout = opts[:connect_timeout] || Config.default_connect_timeout
@read_timeout = opts[:read_timeout] || Config.default_read_timeout
@feature_store = opts[:feature_store] || Config.default_feature_store
@stream = opts.has_key?(:stream) ? opts[:stream] : Config.default_stream
@log_timings = opts.has_key?(:log_timings) ? opts[:log_timings] : Config.default_log_timings
@debug_stream = opts.has_key?(:debug_stream) ? opts[:debug_stream] : Config.default_debug_stream
@offline = opts.has_key?(:offline) ? opts[:offline] : Config.default_offline
@poll_interval = opts.has_key?(:poll_interval) && opts[:poll_interval] > 1 ? opts[:poll_interval] : Config.default_poll_interval
end

#
Expand Down Expand Up @@ -79,13 +84,9 @@ def stream?
@stream
end

#
# Whether we should debug streaming mode. If set, the client will fetch features via polling
# and compare the retrieved feature with the value in the feature store
#
# @return [Boolean] True if we should debug streaming mode
def debug_stream?
@debug_stream
# TODO docs
def offline?
@offline
end

#
Expand All @@ -95,6 +96,11 @@ def debug_stream?
# @return [Float] The configured number of seconds between flushes of the event buffer.
attr_reader :flush_interval

#
# The number of seconds to wait before polling for feature flag updates. This option has no
# effect unless streaming is disabled
attr_reader :poll_interval

#
# The configured logger for the LaunchDarkly client. The client library uses the log to
# print warning and error messages.
Expand All @@ -117,7 +123,7 @@ def debug_stream?
# 'read' and 'write' requests.
#
# @return [Object] The configured store for the Faraday HTTP caching library.
attr_reader :store
attr_reader :cache_store

#
# The read timeout for network connections in seconds.
Expand All @@ -132,16 +138,7 @@ def debug_stream?
attr_reader :connect_timeout

#
# Whether timing information should be logged. If it is logged, it will be logged to the DEBUG
# level on the configured logger. This can be very verbose.
#
# @return [Boolean] True if timing information should be logged.
def log_timings?
@log_timings
end

#
# TODO docs
# A store for feature flag configuration rules.
#
attr_reader :feature_store

Expand Down Expand Up @@ -170,7 +167,7 @@ def self.default_events_uri
"https://events.launchdarkly.com"
end

def self.default_store
def self.default_cache_store
defined?(Rails) && Rails.respond_to?(:cache) ? Rails.cache : ThreadSafeMemoryStore.new
end

Expand All @@ -190,20 +187,20 @@ def self.default_logger
defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : ::Logger.new($stdout)
end

def self.default_log_timings
false
end

def self.default_stream
true
end

def self.default_feature_store
nil
InMemoryFeatureStore.new
end

def self.default_debug_stream
def self.default_offline
false
end

def self.default_poll_interval
1
end
end
end
122 changes: 122 additions & 0 deletions lib/ldclient-rb/evaluation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
module LaunchDarkly

module Evaluation
BUILTINS = [:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]

def param_for_user(feature, user)
return nil unless user[:key]

id_hash = user[:key]
if user[:secondary]
id_hash += "." + user[:secondary]
end

hash_key = "%s.%s.%s" % [feature[:key], feature[:salt], id_hash]

hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
end

def match_target?(target, user)
attrib = target[:attribute].to_sym

if BUILTINS.include?(attrib)
return false unless user[attrib]

u_value = user[attrib]
return target[:values].include? u_value
else # custom attribute
return false unless user[:custom]
return false unless user[:custom].include? attrib

u_value = user[:custom][attrib]
if u_value.is_a? Array
return ! ((target[:values] & u_value).empty?)
else
return target[:values].include? u_value
end

return false
end
end

def match_user?(variation, user)
if variation[:userTarget]
return match_target?(variation[:userTarget], user)
end
false
end

def find_user_match(feature, user)
feature[:variations].each do |variation|
return variation[:value] if match_user?(variation, user)
end
nil
end

def match_variation?(variation, user)
variation[:targets].each do |target|
if !!variation[:userTarget] && target[:attribute].to_sym == :key
next
end

if match_target?(target, user)
return true
end
end
false
end

def find_target_match(feature, user)
feature[:variations].each do |variation|
return variation[:value] if match_variation?(variation, user)
end
nil
end

def find_weight_match(feature, param)
total = 0.0
feature[:variations].each do |variation|
total += variation[:weight].to_f / 100.0

return variation[:value] if param < total
end

nil
end

def evaluate(feature, user)
if feature.nil?
@config.logger.debug("[LDClient] Nil feature in evaluate")
return nil
end

@config.logger.debug("[LDClient] Evaluating feature: #{feature.to_json}")

if !feature[:on]
@config.logger.debug("[LDClient] Feature #{feature[:key]} is off")
return nil
end

param = param_for_user(feature, user)
return nil if param.nil?

value = find_user_match(feature, user)
if !value.nil?
@config.logger.debug("[LDClient] Evaluated feature #{feature[:key]} to #{value} from user targeting match")
return value
end

value = find_target_match(feature, user)
if !value.nil?
@config.logger.debug("[LDClient] Evaluated feature #{feature[:key]} to #{value} from rule match")
return value
end

value = find_weight_match(feature, param)
@config.logger.debug("[LDClient] Evaluated feature #{feature[:key]} to #{value} from percentage rollout")
value
end
end

end
79 changes: 79 additions & 0 deletions lib/ldclient-rb/events.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
require "thread"
require "faraday/http_cache"

module LaunchDarkly

class EventProcessor
def initialize(api_key, config)
@queue = Queue.new
@api_key = api_key
@config = config
@client = Faraday.new do |builder|
builder.use :http_cache, store: @config.cache_store
builder.adapter :net_http_persistent
end

@worker = create_worker
end

def create_worker
Thread.new do
loop do
begin
flush
sleep(@config.flush_interval)
rescue StandardError => exn
log_exception(__method__.to_s, exn)
end
end
end
end

def post_flushed_events(events)
res = @client.post (@config.events_uri + "/bulk") do |req|
req.headers["Authorization"] = "api_key " + @api_key
req.headers["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
req.headers["Content-Type"] = "application/json"
req.body = events.to_json
req.options.timeout = @config.read_timeout
req.options.open_timeout = @config.connect_timeout
end
if res.status / 100 != 2
@config.logger.error("[LDClient] Unexpected status code while processing events: #{res.status}")
end
end

def flush
events = []
begin
loop do
events << @queue.pop(true)
end
rescue ThreadError
end

if !events.empty?
post_flushed_events(events)
end
end

def add_event(event)
return if @offline

if @queue.length < @config.capacity
event[:creationDate] = (Time.now.to_f * 1000).to_i
@config.logger.debug("[LDClient] Enqueueing event: #{event.to_json}")
@queue.push(event)

if !@worker.alive?
@worker = create_worker
end
else
@config.logger.warn("[LDClient] Exceeded event queue capacity. Increase capacity to avoid dropping events.")
end
end

private :create_worker, :post_flushed_events

end
end
Loading