Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f621994
support segments
eli-darkly Jan 17, 2018
5d6c8a8
segment store tests
eli-darkly Jan 17, 2018
4e4f6a3
doc comment
eli-darkly Jan 17, 2018
0648440
misc cleanup, tests
eli-darkly Jan 17, 2018
27855dc
fix omitted parse
eli-darkly Jan 20, 2018
049f68e
fix indirect patch logic - message data is the path
eli-darkly Jan 20, 2018
bc6dc44
Merge branch 'master' into eb/segments
eli-darkly Jan 25, 2018
04fe620
update to use unified generic feature store
eli-darkly Jan 25, 2018
696e42c
comments
eli-darkly Jan 25, 2018
c79a762
misc fixes
eli-darkly Jan 25, 2018
da6c0d6
bump version
eli-darkly Jan 25, 2018
b442243
changelog
eli-darkly Jan 25, 2018
54c45e8
fix test
eli-darkly Jan 25, 2018
23512f4
revert changelog & version
eli-darkly Jan 26, 2018
a5067b0
typo
eli-darkly Jan 31, 2018
7107b16
typo
eli-darkly Jan 31, 2018
9f12e68
typo
eli-darkly Jan 31, 2018
c6094ce
test improvements
eli-darkly Feb 6, 2018
1d3c7e1
Merge branch 'segments' into eb/segments
eli-darkly Feb 6, 2018
70038f5
misc cleanup
eli-darkly Feb 6, 2018
4270c29
add a bunch of unit tests for flag evaluation
eli-darkly Feb 6, 2018
3e4e1b2
misc style cleanup
eli-darkly Feb 6, 2018
0a1c0bd
misc cleanup
eli-darkly Feb 6, 2018
2a6c63a
misc cleanup
eli-darkly Feb 6, 2018
a2e275b
misc cleanup
eli-darkly Feb 7, 2018
ea16971
single quotes, why not
eli-darkly Feb 13, 2018
d0f5492
freeze constant properties
eli-darkly Feb 13, 2018
9dcceeb
minor cleanup
eli-darkly Feb 13, 2018
80caef2
add tests for weight=nil or absent
eli-darkly Feb 13, 2018
57a176c
Merge pull request #33 from launchdarkly/eb/segments
eli-darkly Feb 13, 2018
2c35255
Merge branch 'segments'
eli-darkly Feb 21, 2018
f2c085d
[ch12303] Make pre ruby 2.1.0 faraday dependencies explicit and add j…
ashanbrown Feb 22, 2018
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
20 changes: 11 additions & 9 deletions circle.yml
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
machine:
environment:
RUBIES: "ruby-2.4.1;ruby-2.2.3;ruby-2.1.7;ruby-2.0.0;ruby-1.9.3;jruby-1.7.22"
RUBIES: "ruby-2.4.2 ruby-2.2.7 ruby-2.1.9 ruby-2.0.0 ruby-1.9.3 jruby-1.7.22 jruby-9.0.5.0 jruby-9.1.13.0"
services:
- redis

dependencies:
cache_directories:
- '../.rvm/rubies'
- '/opt/circleci/.rvm/rubies'

override:
- >
rubiesArray=(${RUBIES//;/ });
for i in "${rubiesArray[@]}";
- |
set -e
for i in $RUBIES;
do
rvm install $i;
rvm use $i;
gem install jruby-openssl; # required by bundler, no effect on Ruby MRI
if [[ $i == jruby* ]]; then
gem install jruby-openssl; # required by bundler, no effect on Ruby MRI
fi
gem install bundler;
bundle install;
mv Gemfile.lock "Gemfile.lock.$i"
done

test:
override:
- >
rubiesArray=(${RUBIES//;/ });
for i in "${rubiesArray[@]}";
- |
set -e
for i in $RUBIES;
do
rvm use $i;
cp "Gemfile.lock.$i" Gemfile.lock;
Expand Down
9 changes: 7 additions & 2 deletions ldclient-rb.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,13 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "moneta", "~> 1.0.0"

spec.add_runtime_dependency "json", [">= 1.8", "< 3"]
spec.add_runtime_dependency "faraday", [">= 0.9", "< 2"]
spec.add_runtime_dependency "faraday-http-cache", [">= 1.3.0", "< 3"]
if RUBY_VERSION >= "2.1.0"
spec.add_runtime_dependency "faraday", [">= 0.9", "< 2"]
spec.add_runtime_dependency "faraday-http-cache", [">= 1.3.0", "< 3"]
else
spec.add_runtime_dependency "faraday", [">= 0.9", "< 0.14.0"]
spec.add_runtime_dependency "faraday-http-cache", [">= 1.3.0", "< 2"]
end
spec.add_runtime_dependency "semantic", "~> 1.6.0"
spec.add_runtime_dependency "thread_safe", "~> 0.3"
spec.add_runtime_dependency "net-http-persistent", "~> 2.9"
Expand Down
4 changes: 2 additions & 2 deletions lib/ldclient-rb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
require "ldclient-rb/ldclient"
require "ldclient-rb/cache_store"
require "ldclient-rb/memoized_value"
require "ldclient-rb/in_memory_store"
require "ldclient-rb/config"
require "ldclient-rb/newrelic"
require "ldclient-rb/stream"
require "ldclient-rb/polling"
require "ldclient-rb/event_serializer"
require "ldclient-rb/events"
require "ldclient-rb/feature_store"
require "ldclient-rb/redis_feature_store"
require "ldclient-rb/redis_store"
require "ldclient-rb/requestor"
3 changes: 2 additions & 1 deletion lib/ldclient-rb/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class Config
# @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 [Object] :feature_store A store for feature flags and related data. Defaults to an in-memory
# cache, or you can use RedisFeatureStore.
# @option opts [Boolean] :use_ldd (false) Whether you are using the LaunchDarkly relay proxy in
# daemon mode. In this configuration, the client will not use a streaming connection to listen
# for updates, but instead will get feature state from a Redis instance. The `stream` and
Expand Down Expand Up @@ -171,7 +173,6 @@ def offline?
#
attr_reader :feature_store


# The proxy configuration string
#
attr_reader :proxy
Expand Down
116 changes: 77 additions & 39 deletions lib/ldclient-rb/evaluation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,18 @@ module Evaluation
end

SEMVER_OPERAND = lambda do |v|
semver = nil
if v.is_a? String
for _ in 0..2 do
begin
return Semantic::Version.new(v)
semver = Semantic::Version.new(v)
break # Some versions of jruby cannot properly handle a return here and return from the method that calls this lambda
rescue ArgumentError
v = addZeroVersionComponent(v)
end
end
end
nil
semver
end

def self.addZeroVersionComponent(v)
Expand Down Expand Up @@ -98,7 +100,11 @@ def self.comparator(converter)
semVerLessThan:
comparator(SEMVER_OPERAND) { |n| n < 0 },
semVerGreaterThan:
comparator(SEMVER_OPERAND) { |n| n > 0 }
comparator(SEMVER_OPERAND) { |n| n > 0 },
segmentMatch:
lambda do |a, b|
false # we should never reach this - instead we special-case this operator in clause_match_user
end
}

class EvaluationError < StandardError
Expand Down Expand Up @@ -136,54 +142,46 @@ def evaluate(flag, user, store)
def eval_internal(flag, user, store, events)
failed_prereq = false
# Evaluate prerequisites, if any
if !flag[:prerequisites].nil?
flag[:prerequisites].each do |prerequisite|
prereq_flag = store.get(prerequisite[:key])
(flag[:prerequisites] || []).each do |prerequisite|
prereq_flag = store.get(FEATURES, prerequisite[:key])

if prereq_flag.nil? || !prereq_flag[:on]
failed_prereq = true
else
begin
prereq_res = eval_internal(prereq_flag, user, store, events)
variation = get_variation(prereq_flag, prerequisite[:variation])
events.push(kind: "feature", key: prereq_flag[:key], value: prereq_res, version: prereq_flag[:version], prereqOf: flag[:key])
if prereq_res.nil? || prereq_res != variation
failed_prereq = true
end
rescue => exn
@config.logger.error("[LDClient] Error evaluating prerequisite: #{exn.inspect}")
if prereq_flag.nil? || !prereq_flag[:on]
failed_prereq = true
else
begin
prereq_res = eval_internal(prereq_flag, user, store, events)
variation = get_variation(prereq_flag, prerequisite[:variation])
events.push(kind: "feature", key: prereq_flag[:key], value: prereq_res, version: prereq_flag[:version], prereqOf: flag[:key])
if prereq_res.nil? || prereq_res != variation
failed_prereq = true
end
rescue => exn
@config.logger.error("[LDClient] Error evaluating prerequisite: #{exn.inspect}")
failed_prereq = true
end
end
end

if failed_prereq
return nil
end
if failed_prereq
return nil
end
# The prerequisites were satisfied.
# Now walk through the evaluation steps and get the correct
# variation index
eval_rules(flag, user)
eval_rules(flag, user, store)
end

def eval_rules(flag, user)
def eval_rules(flag, user, store)
# Check user target matches
if !flag[:targets].nil?
flag[:targets].each do |target|
if !target[:values].nil?
target[:values].each do |value|
return get_variation(flag, target[:variation]) if value == user[:key]
end
end
(flag[:targets] || []).each do |target|
(target[:values] || []).each do |value|
return get_variation(flag, target[:variation]) if value == user[:key]
end
end

# Check custom rules
if !flag[:rules].nil?
flag[:rules].each do |rule|
return variation_for_user(rule, user, flag) if rule_match_user(rule, user)
end
(flag[:rules] || []).each do |rule|
return variation_for_user(rule, user, flag) if rule_match_user(rule, user, store)
end

# Check the fallthrough rule
Expand All @@ -202,17 +200,30 @@ def get_variation(flag, index)
flag[:variations][index]
end

def rule_match_user(rule, user)
def rule_match_user(rule, user, store)
return false if !rule[:clauses]

rule[:clauses].each do |clause|
return false if !clause_match_user(clause, user)
(rule[:clauses] || []).each do |clause|
return false if !clause_match_user(clause, user, store)
end

return true
end

def clause_match_user(clause, user)
def clause_match_user(clause, user, store)
# In the case of a segment match operator, we check if the user is in any of the segments,
# and possibly negate
if clause[:op].to_sym == :segmentMatch
(clause[:values] || []).each do |v|
segment = store.get(SEGMENTS, v)
return maybe_negate(clause, true) if !segment.nil? && segment_match_user(segment, user)
end
return maybe_negate(clause, false)
end
clause_match_user_no_segments(clause, user)
end

def clause_match_user_no_segments(clause, user)
val = user_value(user, clause[:attribute])
return false if val.nil?

Expand Down Expand Up @@ -250,6 +261,33 @@ def variation_for_user(rule, user, flag)
end
end

def segment_match_user(segment, user)
return false unless user[:key]

return true if segment[:included].include?(user[:key])
return false if segment[:excluded].include?(user[:key])

(segment[:rules] || []).each do |r|
return true if segment_rule_match_user(r, user, segment[:key], segment[:salt])
end

return false
end

def segment_rule_match_user(rule, user, segment_key, salt)
(rule[:clauses] || []).each do |c|
return false unless clause_match_user_no_segments(c, user)
end

# If the weight is absent, this rule matches
return true if !rule[:weight]

# All of the clauses are met. See if the user buckets in
bucket = bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt)
weight = rule[:weight].to_f / 100000.0
return bucket < weight
end

def bucket_user(user, key, bucket_by, salt)
return nil unless user[:key]

Expand Down
63 changes: 0 additions & 63 deletions lib/ldclient-rb/feature_store.rb

This file was deleted.

Loading