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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +38 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +202 -0
  5. data/README.md +152 -0
  6. data/Rakefile +4 -0
  7. data/lib/splitclient-cache/local_store.rb +45 -0
  8. data/lib/splitclient-engine/evaluator/splitter.rb +110 -0
  9. data/lib/splitclient-engine/impressions/impressions.rb +79 -0
  10. data/lib/splitclient-engine/matchers/all_keys_matcher.rb +46 -0
  11. data/lib/splitclient-engine/matchers/combiners.rb +13 -0
  12. data/lib/splitclient-engine/matchers/combining_matcher.rb +94 -0
  13. data/lib/splitclient-engine/matchers/negation_matcher.rb +54 -0
  14. data/lib/splitclient-engine/matchers/user_defined_segment_matcher.rb +58 -0
  15. data/lib/splitclient-engine/matchers/whitelist_matcher.rb +55 -0
  16. data/lib/splitclient-engine/metrics/binary_search_latency_tracker.rb +122 -0
  17. data/lib/splitclient-engine/metrics/metrics.rb +158 -0
  18. data/lib/splitclient-engine/parser/condition.rb +90 -0
  19. data/lib/splitclient-engine/parser/partition.rb +37 -0
  20. data/lib/splitclient-engine/parser/segment.rb +84 -0
  21. data/lib/splitclient-engine/parser/segment_parser.rb +46 -0
  22. data/lib/splitclient-engine/parser/split.rb +68 -0
  23. data/lib/splitclient-engine/parser/split_adapter.rb +433 -0
  24. data/lib/splitclient-engine/parser/split_parser.rb +129 -0
  25. data/lib/splitclient-engine/partitions/treatments.rb +40 -0
  26. data/lib/splitclient-rb.rb +22 -0
  27. data/lib/splitclient-rb/split_client.rb +170 -0
  28. data/lib/splitclient-rb/split_config.rb +193 -0
  29. data/lib/splitclient-rb/version.rb +3 -0
  30. data/splitclient-rb.gemspec +44 -0
  31. data/tasks/benchmark_is_treatment.rake +37 -0
  32. data/tasks/concurrent_benchmark_is_treatment.rake +43 -0
  33. data/tasks/console.rake +4 -0
  34. data/tasks/rspec.rake +3 -0
  35. 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,13 @@
1
+ module SplitIoClient
2
+
3
+ #
4
+ # class to represent combiner values
5
+ #
6
+ class Combiners < NoMethodError
7
+
8
+ # available combiners of the sdk
9
+ AND = 'AND'
10
+
11
+ end
12
+
13
+ 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