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,129 @@
1
+ module SplitIoClient
2
+ #
3
+ # helper class to parse fetched splits
4
+ #
5
+ class SplitParser < NoMethodError
6
+ #
7
+ # since value for splitChanges last fetch
8
+ attr_accessor :since
9
+
10
+ #
11
+ # till value for splitChanges last fetch
12
+ attr_accessor :till
13
+
14
+ #
15
+ # splits data
16
+ attr_accessor :splits
17
+
18
+ #
19
+ # splits segments data
20
+ attr_accessor :segments
21
+
22
+ def initialize(logger)
23
+ @splits = []
24
+ @since = -1
25
+ @till = -1
26
+ @logger = logger
27
+ end
28
+
29
+ #
30
+ # gets all the split names retrived from the endpoint
31
+ #
32
+ # @return [object] array of split names
33
+ def get_split_names
34
+ @splits.map { |s| s.name }
35
+ end
36
+
37
+ #
38
+ # @return [boolean] true if the splits content is empty false otherwise
39
+ def is_empty?
40
+ @splits.empty? ? true : false
41
+ end
42
+
43
+ #
44
+ # gets all the segment names that are used within the retrieved splits
45
+ #
46
+ # @return [object] array of segment names
47
+ def get_used_segments
48
+ segment_names = []
49
+
50
+ @splits.each { |s|
51
+ s.conditions.each { |c|
52
+ c.matchers.each { |m|
53
+ m[:userDefinedSegmentMatcherData].each { |seg, name|
54
+ segment_names << name
55
+ } unless m[:userDefinedSegmentMatcherData].nil?
56
+ } unless c.matchers.nil?
57
+ }
58
+ }
59
+ segment_names.uniq
60
+ end
61
+
62
+ #
63
+ # gets a split parsed object by name
64
+ #
65
+ # @param name [string] name of the split
66
+ #
67
+ # @return split [object] split object
68
+ def get_split(name)
69
+ @splits.find { |s| s.name == name }
70
+ end
71
+
72
+ #
73
+ # gets the treatment for the given combination of user key and split name
74
+ # using all parsed data for the splits
75
+ #
76
+ # @param id [string] user key
77
+ # @param name [string] split name
78
+ # @param default_treatment [string] default treatment value to be returned
79
+ #
80
+ # @return treatment [object] treatment for this user key, split pair
81
+ def get_split_treatment(id, name, default_treatment)
82
+ split = get_split(name)
83
+
84
+ if !split.is_empty? && split.status == 'ACTIVE' && !split.killed?
85
+ split.conditions.each do |c|
86
+ unless c.is_empty?
87
+ matcher = get_matcher_type(c)
88
+ if matcher.match?(id)
89
+ result = Splitter.get_treatment(id, split.seed, c.partitions) #'true match - running split'
90
+ if result.nil?
91
+ return default_treatment
92
+ else
93
+ return result
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ default_treatment
101
+ end
102
+
103
+ #
104
+ # gets the matcher type from a condition object
105
+ #
106
+ # @param contidion [object] a condition object
107
+ #
108
+ # @return matcher [object] the matcher object for the given condition
109
+ def get_matcher_type(condition)
110
+ final_matcher = nil
111
+
112
+ case condition.matcher
113
+ when 'ALL_KEYS'
114
+ final_matcher = AllKeysMatcher.new
115
+ when 'IN_SEGMENT'
116
+ segment = @segments.get_segment(condition.matcher_segment)
117
+ final_matcher = segment.is_empty? ? UserDefinedSegmentMatcher.new(nil) : UserDefinedSegmentMatcher.new(segment)
118
+ when 'WHITELIST'
119
+ final_matcher = WhitelistMatcher.new(condition.matcher_whitelist)
120
+ else
121
+ @logger.error('Invalid matcher type')
122
+ end
123
+
124
+ final_matcher
125
+ end
126
+
127
+ end
128
+
129
+ end
@@ -0,0 +1,40 @@
1
+ module SplitIoClient
2
+
3
+ #
4
+ # represents the possible return values for a treatment
5
+ #
6
+ class Treatments < NoMethodError
7
+
8
+ # Constants to represent treatment values
9
+ CONTROL = 'control'
10
+ OFF = 'off'
11
+ ON = 'on'
12
+
13
+ # get the actual value for the given treatment type
14
+ #
15
+ # @param type [string] treatment type
16
+ #
17
+ # @return [Treatment] treatment type value
18
+ def self.get_type(type)
19
+ case type
20
+ when 'on'
21
+ return ON
22
+ when 'off', 'control'
23
+ return CONTROL
24
+ else # default return off
25
+ return CONTROL
26
+ end
27
+ end
28
+
29
+ # checks if the give treatment matches control type
30
+ #
31
+ # @param type [string] treatment type
32
+ #
33
+ # @return [boolean] true if matches, false otherwise
34
+ def self.is_control?(treatment)
35
+ get_type(treatment).equal?(CONTROL) ? true : false
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,22 @@
1
+ require 'splitclient-rb/version'
2
+ require 'splitclient-rb/split_client'
3
+ require 'splitclient-rb/split_config'
4
+ require 'splitclient-cache/local_store'
5
+ require 'splitclient-engine/parser/split'
6
+ require 'splitclient-engine/parser/condition'
7
+ require 'splitclient-engine/parser/partition'
8
+ require 'splitclient-engine/parser/segment'
9
+ require 'splitclient-engine/parser/split_adapter'
10
+ require 'splitclient-engine/parser/split_parser'
11
+ require 'splitclient-engine/parser/segment_parser'
12
+ require 'splitclient-engine/partitions/treatments'
13
+ require 'splitclient-engine/matchers/combiners'
14
+ require 'splitclient-engine/matchers/combining_matcher'
15
+ require 'splitclient-engine/matchers/all_keys_matcher'
16
+ require 'splitclient-engine/matchers/negation_matcher'
17
+ require 'splitclient-engine/matchers/user_defined_segment_matcher'
18
+ require 'splitclient-engine/matchers/whitelist_matcher'
19
+ require 'splitclient-engine/evaluator/splitter'
20
+ require 'splitclient-engine/impressions/impressions'
21
+ require 'splitclient-engine/metrics/metrics'
22
+ require 'splitclient-engine/metrics/binary_search_latency_tracker'
@@ -0,0 +1,170 @@
1
+ require 'logger'
2
+
3
+ module SplitIoClient
4
+
5
+ #
6
+ # main class for split client sdk
7
+ #
8
+ class SplitClient < NoMethodError
9
+
10
+ #
11
+ # constant that defines the localhost mode
12
+ LOCALHOST_MODE = 'localhost'
13
+
14
+ #
15
+ # object that acts as an api adapter connector. used to get and post to api endpoints
16
+ attr_reader :adapter
17
+
18
+ #
19
+ # variables to if the sdk is being used in localhost mode and store the list of features
20
+ attr_reader :localhost_mode
21
+ attr_reader :localhost_mode_features
22
+
23
+ #
24
+ # Creates a new split client instance that connects to split.io API.
25
+ #
26
+ # @param api_key [String] the API key for your split account
27
+ #
28
+ # @return [SplitIoClient] split.io client instance
29
+ def initialize(api_key, config = {})
30
+ @localhost_mode = false
31
+ @localhost_mode_features = []
32
+
33
+ @config = SplitConfig.new(config)
34
+
35
+ if api_key == LOCALHOST_MODE
36
+ @localhost_mode = true
37
+ load_localhost_mode_features
38
+ else
39
+ @adapter = SplitAdapter.new(api_key, @config)
40
+ end
41
+ end
42
+
43
+ #
44
+ # validates the treatment for the provided user key and feature
45
+ #
46
+ # @param id [string] user id
47
+ # @param feature [string] name of the feature that is being validated
48
+ # @param treatment [string] value of the treatment for this user key and feature
49
+ #
50
+ # @return [boolean] true if the user key has valida treatment, false otherwise
51
+ def is_treatment?(id, feature, treatment)
52
+ is_treatment = false
53
+
54
+ if is_localhost_mode?
55
+ is_treatment = get_localhost_treatment(feature)
56
+ else
57
+ begin
58
+ is_treatment = (get_treatment(id, feature) == treatment)
59
+ rescue
60
+ @config.logger.error("MUST NOT throw this error")
61
+ end
62
+ end
63
+ is_treatment
64
+ end
65
+
66
+ #
67
+ # obtains the treatment for a given feature
68
+ #
69
+ # @param id [string] user id
70
+ # @param feature [string] name of the feature that is being validated
71
+ #
72
+ # @return [Treatment] treatment constant value
73
+ def get_treatment(id, feature)
74
+ unless id
75
+ @config.logger.warn('id was null for feature: ' + feature)
76
+ return Treatments::CONTROL
77
+ end
78
+
79
+ unless feature
80
+ @config.logger.warn('feature was null for id: ' + id)
81
+ return Treatments::CONTROL
82
+ end
83
+
84
+ start = Time.now
85
+ result = nil
86
+
87
+ begin
88
+ result = get_treatment_without_exception_handling(id, feature)
89
+ rescue StandardError => error
90
+ @config.log_found_exception(__method__.to_s, error)
91
+ end
92
+
93
+ result = result.nil? ? Treatments::CONTROL : result
94
+
95
+ begin
96
+ @adapter.impressions.log(id, feature, result, (Time.now.to_f * 1000.0))
97
+ latency = (Time.now - start) * 1000.0
98
+ if (@adapter.impressions.queue.length >= @adapter.impressions.max_number_of_keys)
99
+ @adapter.impressions_producer.wakeup
100
+ end
101
+ rescue StandardError => error
102
+ @config.log_found_exception(__method__.to_s, error)
103
+ end
104
+
105
+ result
106
+ end
107
+
108
+ #
109
+ # auxiliary method to get the treatments avoding exceptions
110
+ #
111
+ # @param id [string] user id
112
+ # @param feature [string] name of the feature that is being validated
113
+ #
114
+ # @return [Treatment] tretment constant value
115
+ def get_treatment_without_exception_handling(id, feature)
116
+ @adapter.parsed_splits.segments = @adapter.parsed_segments
117
+ split = @adapter.parsed_splits.get_split(feature)
118
+
119
+ if split.nil?
120
+ return Treatments::CONTROL
121
+ else
122
+ default_treatment = split.data[:defaultTreatment]
123
+ return @adapter.parsed_splits.get_split_treatment(id, feature, default_treatment)
124
+ end
125
+ end
126
+
127
+ #
128
+ # method that returns the sdk gem version
129
+ #
130
+ # @return [string] version value for this sdk
131
+ def self.sdk_version
132
+ 'RubyClientSDK-'+SplitIoClient::VERSION
133
+ end
134
+
135
+ #
136
+ # method to check if the sdk is running in localhost mode based on api key
137
+ #
138
+ # @return [boolean] True if is in localhost mode, false otherwise
139
+ def is_localhost_mode?
140
+ @localhost_mode
141
+ end
142
+
143
+ #
144
+ # method to set localhost mode features by reading .splits file located at home directory
145
+ #
146
+ # @returns [void]
147
+ def load_localhost_mode_features
148
+ splits_file = File.join(Dir.home, ".splits")
149
+ if File.exists?(splits_file)
150
+ line_num=0
151
+ File.open(splits_file).each do |line|
152
+ @localhost_mode_features << line.strip unless line.start_with?('#') || line.strip.empty?
153
+ end
154
+ end
155
+ end
156
+
157
+ #
158
+ # method to check the treatment for the given feature in localhost mode
159
+ #
160
+ # @return [boolean] true if the feature is available in localhost mode, false otherwise
161
+ def get_localhost_treatment(feature)
162
+ @localhost_mode_features.include?(feature)
163
+ end
164
+
165
+ private :get_treatment_without_exception_handling, :is_localhost_mode?,
166
+ :load_localhost_mode_features, :get_localhost_treatment
167
+
168
+ end
169
+
170
+ end
@@ -0,0 +1,193 @@
1
+ require 'logger'
2
+ require 'socket'
3
+
4
+ module SplitIoClient
5
+ #
6
+ # This class manages configuration options for the split client library.
7
+ # If not custom configuration is required the default configuration values will be used
8
+ #
9
+ class SplitConfig
10
+ #
11
+ # Constructor for creating custom split client config
12
+ #
13
+ # @param opts [Hash] optional hash with configuration options
14
+ # @option opts [String] :base_uri ("https://sdk.split.io/api/") The base URL for split API end points
15
+ # @option opts [Int] :read_timeout (10) The read timeout for network connections in seconds.
16
+ # @option opts [Int] :connection_timeout (2) The connect timeout for network connections in seconds.
17
+ # @option opts [Object] :local_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.
18
+ # @option opts [Int] :features_refresh_rate The SDK polls Split servers for changes to feature roll-out plans. This parameter controls this polling period in seconds.
19
+ # @option opts [Int] :segments_refresh_rate
20
+ # @option opts [Int] :metrics_refresh_rate
21
+ # @option opts [Int] :impressions_refresh_rate
22
+ # @option opts [Object] :logger a logger to user for messages from the client. Defaults to stdout
23
+ # @option opts [Boolean] :debug_enabled (false) The value for the debug flag
24
+ #
25
+ # @return [type] SplitConfig with configuration options
26
+ def initialize(opts = {})
27
+ @base_uri = (opts[:base_uri] || SplitConfig.default_base_uri).chomp('/')
28
+ @local_store = opts[:local_store] || SplitConfig.default_local_store
29
+ @connection_timeout = opts[:connection_timeout] || SplitConfig.default_connection_timeout
30
+ @read_timeout = opts[:read_timeout] || SplitConfig.default_read_timeout
31
+ @features_refresh_rate = opts[:features_refresh_rate] || SplitConfig.default_features_refresh_rate
32
+ @segments_refresh_rate = opts[:segments_refresh_rate] || SplitConfig.default_segments_refresh_rate
33
+ @metrics_refresh_rate = opts[:metrics_refresh_rate] || SplitConfig.default_metrics_refresh_rate
34
+ @impressions_refresh_rate = opts[:impressions_refresh_rate] || SplitConfig.default_impressions_refresh_rate
35
+ @logger = opts[:logger] || SplitConfig.default_logger
36
+ @debug_enabled = opts[:debug_enabled] || SplitConfig.default_debug
37
+ @machine_name = SplitConfig.get_hostname
38
+ @machine_ip = SplitConfig.get_ip
39
+ end
40
+
41
+ #
42
+ # The base URL for split API end points
43
+ #
44
+ # @return [String] The configured base URL for the split API end points
45
+ attr_reader :base_uri
46
+
47
+ #
48
+ # The store for the Faraday HTTP caching library. Stores should respond to
49
+ # 'read', 'write' and 'delete' requests.
50
+ #
51
+ # @return [Object] The configured store for the Faraday HTTP caching library.
52
+ attr_reader :local_store
53
+
54
+ #
55
+ # The read timeout for network connections in seconds.
56
+ #
57
+ # @return [Int] The timeout in seconds.
58
+ attr_reader :read_timeout
59
+
60
+ #
61
+ # The connection timeout for network connections in seconds.
62
+ #
63
+ # @return [Int] The connect timeout in seconds.
64
+ attr_reader :connection_timeout
65
+
66
+ #
67
+ # The configured logger. The client library uses the log to
68
+ # print warning and error messages.
69
+ #
70
+ # @return [Logger] The configured logger
71
+ attr_reader :logger
72
+
73
+ #
74
+ # The boolean that represents the state of the debug log level
75
+ #
76
+ # @return [Boolean] The value for the debug flag
77
+ attr_reader :debug_enabled
78
+
79
+ attr_reader :machine_ip
80
+ attr_reader :machine_name
81
+
82
+ attr_reader :features_refresh_rate
83
+ attr_reader :segments_refresh_rate
84
+ attr_reader :metrics_refresh_rate
85
+ attr_reader :impressions_refresh_rate
86
+
87
+ #
88
+ # The default split client configuration
89
+ #
90
+ # @return [Config] The default split client configuration.
91
+ def self.default
92
+ SplitConfig.new
93
+ end
94
+
95
+ #
96
+ # The default base uri for api calls
97
+ #
98
+ # @return [string] The default base uri
99
+ def self.default_base_uri
100
+ 'https://sdk.split.io/api/'
101
+ end
102
+
103
+ # @return [LocalStore] configuration value for local cache store
104
+ def self.default_local_store
105
+ defined?(Rails) && Rails.respond_to?(:cache) ? Rails.cache : LocalStore.new
106
+ end
107
+
108
+ #
109
+ # The default read timeout value
110
+ #
111
+ # @return [int]
112
+ def self.default_read_timeout
113
+ 5
114
+ end
115
+
116
+ #
117
+ # The default connection timeout value
118
+ #
119
+ # @return [int]
120
+ def self.default_connection_timeout
121
+ 5
122
+ end
123
+
124
+ def self.default_features_refresh_rate
125
+ 30
126
+ end
127
+
128
+ def self.default_segments_refresh_rate
129
+ 60
130
+ end
131
+
132
+ def self.default_metrics_refresh_rate
133
+ 60
134
+ end
135
+
136
+ def self.default_impressions_refresh_rate
137
+ 60
138
+ end
139
+
140
+ #
141
+ # The default logger object
142
+ #
143
+ # @return [object]
144
+ def self.default_logger
145
+ Logger.new($stdout)
146
+ end
147
+
148
+ #
149
+ # The default debug value
150
+ #
151
+ # @return [boolean]
152
+ def self.default_debug
153
+ false
154
+ end
155
+
156
+ #
157
+ # custom logger of exceptions
158
+ #
159
+ # @return [void]
160
+ def log_found_exception(caller, exn)
161
+ error_traceback = "#{exn.inspect} #{exn}\n\t#{exn.backtrace.join("\n\t")}"
162
+ error = "[splitclient-rb] Unexpected exception in #{caller}: #{error_traceback}"
163
+ @logger.error(error)
164
+ end
165
+
166
+ #
167
+ # gets the hostname where the sdk gem is running
168
+ #
169
+ # @return [string]
170
+ def self.get_hostname
171
+ begin
172
+ Socket.gethostname
173
+ rescue
174
+ #unable to get hostname
175
+ 'localhost'
176
+ end
177
+ end
178
+
179
+ #
180
+ # gets the ip where the sdk gem is running
181
+ #
182
+ # @return [string]
183
+ def self.get_ip
184
+ begin
185
+ Socket::getaddrinfo(Socket.gethostname, 'echo', Socket::AF_INET)[0][3]
186
+ rescue
187
+ #unable to get local ip
188
+ '127.0.0.0'
189
+ end
190
+ end
191
+
192
+ end
193
+ end