splitclient-rb 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +38 -0
- data/Gemfile +4 -0
- data/LICENSE +202 -0
- data/README.md +152 -0
- data/Rakefile +4 -0
- data/lib/splitclient-cache/local_store.rb +45 -0
- data/lib/splitclient-engine/evaluator/splitter.rb +110 -0
- data/lib/splitclient-engine/impressions/impressions.rb +79 -0
- data/lib/splitclient-engine/matchers/all_keys_matcher.rb +46 -0
- data/lib/splitclient-engine/matchers/combiners.rb +13 -0
- data/lib/splitclient-engine/matchers/combining_matcher.rb +94 -0
- data/lib/splitclient-engine/matchers/negation_matcher.rb +54 -0
- data/lib/splitclient-engine/matchers/user_defined_segment_matcher.rb +58 -0
- data/lib/splitclient-engine/matchers/whitelist_matcher.rb +55 -0
- data/lib/splitclient-engine/metrics/binary_search_latency_tracker.rb +122 -0
- data/lib/splitclient-engine/metrics/metrics.rb +158 -0
- data/lib/splitclient-engine/parser/condition.rb +90 -0
- data/lib/splitclient-engine/parser/partition.rb +37 -0
- data/lib/splitclient-engine/parser/segment.rb +84 -0
- data/lib/splitclient-engine/parser/segment_parser.rb +46 -0
- data/lib/splitclient-engine/parser/split.rb +68 -0
- data/lib/splitclient-engine/parser/split_adapter.rb +433 -0
- data/lib/splitclient-engine/parser/split_parser.rb +129 -0
- data/lib/splitclient-engine/partitions/treatments.rb +40 -0
- data/lib/splitclient-rb.rb +22 -0
- data/lib/splitclient-rb/split_client.rb +170 -0
- data/lib/splitclient-rb/split_config.rb +193 -0
- data/lib/splitclient-rb/version.rb +3 -0
- data/splitclient-rb.gemspec +44 -0
- data/tasks/benchmark_is_treatment.rake +37 -0
- data/tasks/concurrent_benchmark_is_treatment.rake +43 -0
- data/tasks/console.rake +4 -0
- data/tasks/rspec.rake +3 -0
- metadata +260 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
# Misc class in charge of providing hash functions and
|
|
3
|
+
# determination of treatment based on concept of buckets
|
|
4
|
+
# based on provided key
|
|
5
|
+
#
|
|
6
|
+
class Splitter < NoMethodError
|
|
7
|
+
|
|
8
|
+
#
|
|
9
|
+
# Checks if the partiotion size is 100%
|
|
10
|
+
#
|
|
11
|
+
# @param partitions [object] array of partitions
|
|
12
|
+
#
|
|
13
|
+
# @return [boolean] true if partition is 100% false otherwise
|
|
14
|
+
def self.hundred_percent_one_treatment?(partitions)
|
|
15
|
+
if partitions.size != 1
|
|
16
|
+
return false
|
|
17
|
+
end
|
|
18
|
+
return (partitions.first).size == 100
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# gets the appropriate treatment based on id, seed and partition value
|
|
24
|
+
#
|
|
25
|
+
# @param id [string] user key
|
|
26
|
+
# @param seed [number] seed for the user key
|
|
27
|
+
# @param partitions [object] array of partitions
|
|
28
|
+
#
|
|
29
|
+
# @return traetment [object] treatment value
|
|
30
|
+
def self.get_treatment(id, seed, partitions)
|
|
31
|
+
if partitions.empty?
|
|
32
|
+
return Treatments::CONTROL
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if hundred_percent_one_treatment?(partitions)
|
|
36
|
+
return (partitions.first).treatment
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
return get_treatment_for_key(bucket(hash(id, seed)), partitions)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
#
|
|
43
|
+
# returns a hash value for the give key, sedd pair
|
|
44
|
+
#
|
|
45
|
+
# @param key [string] user key
|
|
46
|
+
# @param seed [number] seed for the user key
|
|
47
|
+
#
|
|
48
|
+
# @return hash [string] hash value
|
|
49
|
+
def self.hash(key, seed)
|
|
50
|
+
h = 0
|
|
51
|
+
for i in 0..key.length-1
|
|
52
|
+
h = to_int32(31 * h + key[i].ord)
|
|
53
|
+
end
|
|
54
|
+
h^seed
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
#
|
|
58
|
+
# misc method to convert ruby number to int 32 since overflow is handled different to java
|
|
59
|
+
#
|
|
60
|
+
# @param number [number] ruby number value
|
|
61
|
+
#
|
|
62
|
+
# @return [int] returns the int 32 value of the provided number
|
|
63
|
+
def self.to_int32(number)
|
|
64
|
+
begin
|
|
65
|
+
sign = number < 0 ? -1 : 1
|
|
66
|
+
abs = number.abs
|
|
67
|
+
return 0 if abs == 0 || abs == Float::INFINITY
|
|
68
|
+
rescue
|
|
69
|
+
return 0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
pos_int = sign * abs.floor
|
|
73
|
+
int_32bit = pos_int % 2**32
|
|
74
|
+
|
|
75
|
+
return int_32bit - 2**32 if int_32bit >= 2**31
|
|
76
|
+
int_32bit
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
#
|
|
80
|
+
# returns the treatment for a bucket given the partitions
|
|
81
|
+
#
|
|
82
|
+
# @param bucket [number] bucket value
|
|
83
|
+
# @param parittions [object] array of partitions
|
|
84
|
+
#
|
|
85
|
+
# @return treatment [treatment] treatment value for this bucket and partitions
|
|
86
|
+
def self.get_treatment_for_key(bucket, partitions)
|
|
87
|
+
buckets_covered_thus_far = 0
|
|
88
|
+
partitions.each do |p|
|
|
89
|
+
unless p.is_empty?
|
|
90
|
+
buckets_covered_thus_far += p.size
|
|
91
|
+
return p.treatment if buckets_covered_thus_far >= bucket
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
return Treatments::CONTROL
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
#
|
|
99
|
+
# returns bucket value for the given hash value
|
|
100
|
+
#
|
|
101
|
+
# @param hash_value [string] hash value
|
|
102
|
+
#
|
|
103
|
+
# @return bucket [number] bucket number
|
|
104
|
+
def self.bucket(hash_value)
|
|
105
|
+
(hash_value.abs % 100) + 1
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# class to manage cached impressions
|
|
5
|
+
#
|
|
6
|
+
class Impressions < NoMethodError
|
|
7
|
+
|
|
8
|
+
# the queue of cached impression values
|
|
9
|
+
#
|
|
10
|
+
# @return [object] array of impressions
|
|
11
|
+
attr_accessor :queue
|
|
12
|
+
|
|
13
|
+
# max number of cached entries for impressions
|
|
14
|
+
#
|
|
15
|
+
# @return [int] max numbre of entries
|
|
16
|
+
attr_accessor :max_number_of_keys
|
|
17
|
+
|
|
18
|
+
#
|
|
19
|
+
# initializes the class
|
|
20
|
+
#
|
|
21
|
+
# @param max [int] max number of cached entries
|
|
22
|
+
def initialize(max)
|
|
23
|
+
@queue = Queue.new
|
|
24
|
+
@max_number_of_keys = max
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
#
|
|
28
|
+
# generates a new entry for impressions list
|
|
29
|
+
#
|
|
30
|
+
# @param id [string] user key
|
|
31
|
+
# @param feature [string] feature name
|
|
32
|
+
# @param treatment [string] treatment value
|
|
33
|
+
# @param time [time] time value in milisenconds
|
|
34
|
+
#
|
|
35
|
+
# @return void
|
|
36
|
+
def log(id, feature, treatment, time)
|
|
37
|
+
impressions = KeyImpressions.new(id, treatment, time)
|
|
38
|
+
@queue << {feature: feature, impressions: impressions}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#
|
|
42
|
+
# clears the impressions queue
|
|
43
|
+
#
|
|
44
|
+
# @returns void
|
|
45
|
+
def clear
|
|
46
|
+
popped_impressions = []
|
|
47
|
+
begin
|
|
48
|
+
loop do
|
|
49
|
+
impression_element = @queue.pop(true)
|
|
50
|
+
feature_hash = popped_impressions.find { |i| i[:feature] == impression_element[:feature] }
|
|
51
|
+
if feature_hash.nil?
|
|
52
|
+
popped_impressions << {feature: impression_element[:feature], impressions: [] << impression_element[:impressions]}
|
|
53
|
+
else
|
|
54
|
+
feature_hash[:impressions] << impression_element[:impressions]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
rescue ThreadError
|
|
58
|
+
end
|
|
59
|
+
popped_impressions
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#
|
|
65
|
+
# small class to use as DTO for impressions
|
|
66
|
+
#
|
|
67
|
+
class KeyImpressions
|
|
68
|
+
attr_accessor :key
|
|
69
|
+
attr_accessor :treatment
|
|
70
|
+
attr_accessor :time
|
|
71
|
+
|
|
72
|
+
def initialize(key, treatment, time)
|
|
73
|
+
@key = key
|
|
74
|
+
@treatment = treatment
|
|
75
|
+
@time = time
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# class to implement the all keys matcher
|
|
5
|
+
#
|
|
6
|
+
class AllKeysMatcher < NoMethodError
|
|
7
|
+
|
|
8
|
+
#
|
|
9
|
+
# evaluates if the key matches the matcher
|
|
10
|
+
#
|
|
11
|
+
# @param key [string] key value to be matched
|
|
12
|
+
#
|
|
13
|
+
# @return [boolean] true for all instances
|
|
14
|
+
def match?(key)
|
|
15
|
+
true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
#
|
|
19
|
+
# evaluates if the given object equals the matcher
|
|
20
|
+
#
|
|
21
|
+
# @param obj [object] object to be evaluated
|
|
22
|
+
#
|
|
23
|
+
# @returns [boolean] true if obj equals the matcher
|
|
24
|
+
def equals?(obj)
|
|
25
|
+
if obj.nil?
|
|
26
|
+
false
|
|
27
|
+
elsif self.equal?(obj)
|
|
28
|
+
true
|
|
29
|
+
elsif !obj.instance_of?(AllKeysMatcher)
|
|
30
|
+
false
|
|
31
|
+
else
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#
|
|
37
|
+
# function to print string value for this matcher
|
|
38
|
+
#
|
|
39
|
+
# @reutrn [string] string value of this matcher
|
|
40
|
+
def to_s
|
|
41
|
+
'in segment all'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require 'splitclient-engine/matchers/combiners'
|
|
2
|
+
|
|
3
|
+
module SplitIoClient
|
|
4
|
+
#
|
|
5
|
+
# class to implement the combining matcher
|
|
6
|
+
#
|
|
7
|
+
class CombiningMatcher < NoMethodError
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# list of matcher within the combiner
|
|
11
|
+
#
|
|
12
|
+
@matcher_list = []
|
|
13
|
+
|
|
14
|
+
#
|
|
15
|
+
# combiner value
|
|
16
|
+
#
|
|
17
|
+
@combiner = ''
|
|
18
|
+
|
|
19
|
+
def initialize(combiner, delegates)
|
|
20
|
+
unless delegates.nil?
|
|
21
|
+
@matcher_list = delegates
|
|
22
|
+
end
|
|
23
|
+
unless combiner.nil?
|
|
24
|
+
@combiner = combiner
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#
|
|
29
|
+
# evaluates if the key matches the matchers within the combiner
|
|
30
|
+
#
|
|
31
|
+
# @param key [string] key value to be matched
|
|
32
|
+
#
|
|
33
|
+
# @return [boolean] match value for combiner delegates
|
|
34
|
+
def match?(key)
|
|
35
|
+
if @matcher_list.empty?
|
|
36
|
+
return false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
case @combiner
|
|
40
|
+
when Combiners::AND
|
|
41
|
+
return and_eval(key)
|
|
42
|
+
else
|
|
43
|
+
#throws error
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
#
|
|
48
|
+
# auxiliary method to evaluate each of the matchers within the combiner
|
|
49
|
+
#
|
|
50
|
+
# @param key [string] key value to be matched
|
|
51
|
+
#
|
|
52
|
+
# @return [boolean] match value for combiner delegates
|
|
53
|
+
def and_eval(key)
|
|
54
|
+
result = true
|
|
55
|
+
@matcher_list.each do |delegate|
|
|
56
|
+
result &= (delegate.match?(key))
|
|
57
|
+
end
|
|
58
|
+
result
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#
|
|
62
|
+
# evaluates if the given object equals the matcher
|
|
63
|
+
#
|
|
64
|
+
# @param obj [object] object to be evaluated
|
|
65
|
+
#
|
|
66
|
+
# @returns [boolean] true if obj equals the matcher
|
|
67
|
+
def equals?(obj)
|
|
68
|
+
if obj.nil?
|
|
69
|
+
false
|
|
70
|
+
elsif !obj.instance_of?(CombiningMatcher)
|
|
71
|
+
false
|
|
72
|
+
elsif self.equal?(obj)
|
|
73
|
+
true
|
|
74
|
+
else
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
#
|
|
80
|
+
# function to print string value for this matcher
|
|
81
|
+
#
|
|
82
|
+
# @reutrn [string] string value of this matcher
|
|
83
|
+
def to_s
|
|
84
|
+
result = ''
|
|
85
|
+
@matcher_list.each_with_index do |matcher, i|
|
|
86
|
+
result += matcher.to_s
|
|
87
|
+
result += ' ' + @combiner if i != 0
|
|
88
|
+
end
|
|
89
|
+
result
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# class to implement the negation of a matcher
|
|
5
|
+
#
|
|
6
|
+
class NegationMatcher < NoMethodError
|
|
7
|
+
|
|
8
|
+
@matcher = nil
|
|
9
|
+
|
|
10
|
+
def initialize(matcher)
|
|
11
|
+
unless matcher.nil?
|
|
12
|
+
@matcher = matcher
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# evaluates if the key matches the negation of the matcher
|
|
18
|
+
#
|
|
19
|
+
# @param key [string] key value to be matched
|
|
20
|
+
#
|
|
21
|
+
# @return [boolean] evaluation of the negation matcher
|
|
22
|
+
def match?(key)
|
|
23
|
+
!@matcher.match?(key)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# evaluates if the given object equals the matcher
|
|
28
|
+
#
|
|
29
|
+
# @param obj [object] object to be evaluated
|
|
30
|
+
#
|
|
31
|
+
# @returns [boolean] true if obj equals the matcher
|
|
32
|
+
def equals?(obj)
|
|
33
|
+
if obj.nil?
|
|
34
|
+
false
|
|
35
|
+
elsif !obj.instance_of?(NegationMatcher)
|
|
36
|
+
false
|
|
37
|
+
elsif self.equal?(obj)
|
|
38
|
+
true
|
|
39
|
+
else
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#
|
|
45
|
+
# function to print string value for this matcher
|
|
46
|
+
#
|
|
47
|
+
# @reutrn [string] string value of this matcher
|
|
48
|
+
def to_s
|
|
49
|
+
'not ' + @matcher.to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# class to implement the user defined matcher
|
|
5
|
+
#
|
|
6
|
+
class UserDefinedSegmentMatcher < NoMethodError
|
|
7
|
+
|
|
8
|
+
@segment = nil
|
|
9
|
+
|
|
10
|
+
def initialize(segment)
|
|
11
|
+
unless segment.nil?
|
|
12
|
+
@segment = segment
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# evaluates if the key matches the matcher
|
|
18
|
+
#
|
|
19
|
+
# @param key [string] key value to be matched
|
|
20
|
+
#
|
|
21
|
+
# @return [boolean] evaluation of the key against the segment
|
|
22
|
+
def match?(key)
|
|
23
|
+
matches = false
|
|
24
|
+
unless @segment.users.nil?
|
|
25
|
+
matches = @segment.users.include?(key)
|
|
26
|
+
end
|
|
27
|
+
matches
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
#
|
|
31
|
+
# evaluates if the given object equals the matcher
|
|
32
|
+
#
|
|
33
|
+
# @param obj [object] object to be evaluated
|
|
34
|
+
#
|
|
35
|
+
# @returns [boolean] true if obj equals the matcher
|
|
36
|
+
def equals?(obj)
|
|
37
|
+
if obj.nil?
|
|
38
|
+
false
|
|
39
|
+
elsif !obj.instance_of?(UserDefinedSegmentMatcher)
|
|
40
|
+
false
|
|
41
|
+
elsif self.equal?(obj)
|
|
42
|
+
true
|
|
43
|
+
else
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
#
|
|
49
|
+
# function to print string value for this matcher
|
|
50
|
+
#
|
|
51
|
+
# @reutrn [string] string value of this matcher
|
|
52
|
+
def to_s
|
|
53
|
+
'in segment ' + @segment.name
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|