splitclient-rb 4.5.1-java
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 +45 -0
- data/CHANGES.txt +147 -0
- data/Detailed-README.md +571 -0
- data/Gemfile +4 -0
- data/LICENSE +13 -0
- data/NEWS +75 -0
- data/README.md +43 -0
- data/Rakefile +24 -0
- data/exe/splitio +96 -0
- data/ext/murmurhash/MurmurHash3.java +162 -0
- data/lib/murmurhash/base.rb +58 -0
- data/lib/murmurhash/murmurhash.jar +0 -0
- data/lib/murmurhash/murmurhash_mri.rb +3 -0
- data/lib/splitclient-rb.rb +90 -0
- data/lib/splitclient-rb/cache/adapters/memory_adapter.rb +12 -0
- data/lib/splitclient-rb/cache/adapters/memory_adapters/map_adapter.rb +133 -0
- data/lib/splitclient-rb/cache/adapters/memory_adapters/queue_adapter.rb +44 -0
- data/lib/splitclient-rb/cache/adapters/redis_adapter.rb +165 -0
- data/lib/splitclient-rb/cache/repositories/events/memory_repository.rb +30 -0
- data/lib/splitclient-rb/cache/repositories/events/redis_repository.rb +29 -0
- data/lib/splitclient-rb/cache/repositories/events_repository.rb +41 -0
- data/lib/splitclient-rb/cache/repositories/impressions/memory_repository.rb +49 -0
- data/lib/splitclient-rb/cache/repositories/impressions/redis_repository.rb +78 -0
- data/lib/splitclient-rb/cache/repositories/impressions_repository.rb +21 -0
- data/lib/splitclient-rb/cache/repositories/metrics/memory_repository.rb +129 -0
- data/lib/splitclient-rb/cache/repositories/metrics/redis_repository.rb +98 -0
- data/lib/splitclient-rb/cache/repositories/metrics_repository.rb +22 -0
- data/lib/splitclient-rb/cache/repositories/repository.rb +23 -0
- data/lib/splitclient-rb/cache/repositories/segments_repository.rb +82 -0
- data/lib/splitclient-rb/cache/repositories/splits_repository.rb +106 -0
- data/lib/splitclient-rb/cache/routers/impression_router.rb +52 -0
- data/lib/splitclient-rb/cache/senders/events_sender.rb +47 -0
- data/lib/splitclient-rb/cache/senders/impressions_formatter.rb +73 -0
- data/lib/splitclient-rb/cache/senders/impressions_sender.rb +67 -0
- data/lib/splitclient-rb/cache/senders/metrics_sender.rb +49 -0
- data/lib/splitclient-rb/cache/stores/sdk_blocker.rb +48 -0
- data/lib/splitclient-rb/cache/stores/segment_store.rb +82 -0
- data/lib/splitclient-rb/cache/stores/split_store.rb +97 -0
- data/lib/splitclient-rb/clients/localhost_split_client.rb +92 -0
- data/lib/splitclient-rb/clients/split_client.rb +214 -0
- data/lib/splitclient-rb/engine/api/client.rb +74 -0
- data/lib/splitclient-rb/engine/api/events.rb +48 -0
- data/lib/splitclient-rb/engine/api/faraday_middleware/gzip.rb +55 -0
- data/lib/splitclient-rb/engine/api/impressions.rb +42 -0
- data/lib/splitclient-rb/engine/api/metrics.rb +61 -0
- data/lib/splitclient-rb/engine/api/segments.rb +62 -0
- data/lib/splitclient-rb/engine/api/splits.rb +60 -0
- data/lib/splitclient-rb/engine/evaluator/splitter.rb +123 -0
- data/lib/splitclient-rb/engine/matchers/all_keys_matcher.rb +46 -0
- data/lib/splitclient-rb/engine/matchers/between_matcher.rb +56 -0
- data/lib/splitclient-rb/engine/matchers/combiners.rb +9 -0
- data/lib/splitclient-rb/engine/matchers/combining_matcher.rb +86 -0
- data/lib/splitclient-rb/engine/matchers/contains_all_matcher.rb +21 -0
- data/lib/splitclient-rb/engine/matchers/contains_any_matcher.rb +19 -0
- data/lib/splitclient-rb/engine/matchers/contains_matcher.rb +30 -0
- data/lib/splitclient-rb/engine/matchers/dependency_matcher.rb +20 -0
- data/lib/splitclient-rb/engine/matchers/ends_with_matcher.rb +26 -0
- data/lib/splitclient-rb/engine/matchers/equal_to_boolean_matcher.rb +27 -0
- data/lib/splitclient-rb/engine/matchers/equal_to_matcher.rb +54 -0
- data/lib/splitclient-rb/engine/matchers/equal_to_set_matcher.rb +19 -0
- data/lib/splitclient-rb/engine/matchers/greater_than_or_equal_to_matcher.rb +53 -0
- data/lib/splitclient-rb/engine/matchers/less_than_or_equal_to_matcher.rb +53 -0
- data/lib/splitclient-rb/engine/matchers/matches_string_matcher.rb +24 -0
- data/lib/splitclient-rb/engine/matchers/negation_matcher.rb +60 -0
- data/lib/splitclient-rb/engine/matchers/part_of_set_matcher.rb +23 -0
- data/lib/splitclient-rb/engine/matchers/set_matcher.rb +20 -0
- data/lib/splitclient-rb/engine/matchers/starts_with_matcher.rb +26 -0
- data/lib/splitclient-rb/engine/matchers/user_defined_segment_matcher.rb +45 -0
- data/lib/splitclient-rb/engine/matchers/whitelist_matcher.rb +66 -0
- data/lib/splitclient-rb/engine/metrics/binary_search_latency_tracker.rb +128 -0
- data/lib/splitclient-rb/engine/metrics/metrics.rb +83 -0
- data/lib/splitclient-rb/engine/models/label.rb +8 -0
- data/lib/splitclient-rb/engine/models/split.rb +17 -0
- data/lib/splitclient-rb/engine/models/treatment.rb +3 -0
- data/lib/splitclient-rb/engine/parser/condition.rb +210 -0
- data/lib/splitclient-rb/engine/parser/evaluator.rb +118 -0
- data/lib/splitclient-rb/engine/parser/partition.rb +35 -0
- data/lib/splitclient-rb/engine/parser/split_adapter.rb +88 -0
- data/lib/splitclient-rb/exceptions/impressions_shutdown_exception.rb +4 -0
- data/lib/splitclient-rb/exceptions/sdk_blocker_timeout_expired_exception.rb +4 -0
- data/lib/splitclient-rb/localhost_split_factory.rb +13 -0
- data/lib/splitclient-rb/localhost_utils.rb +36 -0
- data/lib/splitclient-rb/managers/localhost_split_manager.rb +45 -0
- data/lib/splitclient-rb/managers/split_manager.rb +77 -0
- data/lib/splitclient-rb/split_config.rb +391 -0
- data/lib/splitclient-rb/split_factory.rb +35 -0
- data/lib/splitclient-rb/split_factory_builder.rb +16 -0
- data/lib/splitclient-rb/utilitites.rb +41 -0
- data/lib/splitclient-rb/version.rb +3 -0
- data/splitclient-rb.gemspec +50 -0
- data/splitio.yml.example +7 -0
- data/tasks/benchmark_get_treatment.rake +43 -0
- data/tasks/irb.rake +4 -0
- data/tasks/rspec.rake +3 -0
- metadata +321 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require 'net/http/persistent'
|
|
2
|
+
|
|
3
|
+
module SplitIoClient
|
|
4
|
+
module Api
|
|
5
|
+
class Client
|
|
6
|
+
RUBY_ENCODING = '1.9'.respond_to?(:force_encoding)
|
|
7
|
+
|
|
8
|
+
def get_api(url, config, api_key, params = {})
|
|
9
|
+
api_client.get(url, params) do |req|
|
|
10
|
+
req.headers = common_headers(api_key, config).merge('Accept-Encoding' => 'gzip')
|
|
11
|
+
|
|
12
|
+
req.options[:timeout] = config.read_timeout
|
|
13
|
+
req.options[:open_timeout] = config.connection_timeout
|
|
14
|
+
|
|
15
|
+
config.logger.debug("GET #{url} proxy: #{api_client.proxy}") if config.debug_enabled
|
|
16
|
+
end
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
config.logger.warn("#{e}\nURL:#{url}\nparams:#{params}")
|
|
19
|
+
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def post_api(url, config, api_key, data, headers = {}, params = {})
|
|
24
|
+
api_client.post(url) do |req|
|
|
25
|
+
req.headers = common_headers(api_key, config)
|
|
26
|
+
.merge('Content-Type' => 'application/json')
|
|
27
|
+
.merge(headers)
|
|
28
|
+
|
|
29
|
+
req.body = data.to_json
|
|
30
|
+
|
|
31
|
+
req.options[:timeout] = config.read_timeout
|
|
32
|
+
req.options[:open_timeout] = config.connection_timeout
|
|
33
|
+
|
|
34
|
+
if config.transport_debug_enabled
|
|
35
|
+
config.logger.debug("POST #{url} #{req.body}")
|
|
36
|
+
elsif config.debug_enabled
|
|
37
|
+
config.logger.debug("POST #{url}")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
config.logger.warn("#{e}\nURL:#{url}\ndata:#{data}\nparams:#{params}")
|
|
42
|
+
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def api_client
|
|
49
|
+
@api_client ||= Faraday.new do |builder|
|
|
50
|
+
builder.use SplitIoClient::FaradayMiddleware::Gzip
|
|
51
|
+
builder.adapter :net_http_persistent
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def common_headers(api_key, config)
|
|
56
|
+
{
|
|
57
|
+
'Authorization' => "Bearer #{api_key}",
|
|
58
|
+
'SplitSDKVersion' => "#{config.language}-#{config.version}",
|
|
59
|
+
'SplitSDKMachineName' => config.machine_name,
|
|
60
|
+
'SplitSDKMachineIP' => config.machine_ip,
|
|
61
|
+
'Referer' => referer(config)
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def referer(config)
|
|
66
|
+
result = "#{config.language}-#{config.version}"
|
|
67
|
+
|
|
68
|
+
result = "#{result}::#{SplitIoClient::SplitConfig.machine_hostname}" unless SplitIoClient::SplitConfig.machine_hostname == 'localhost'
|
|
69
|
+
|
|
70
|
+
result
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
module Api
|
|
3
|
+
class Events < Client
|
|
4
|
+
def initialize(api_key, config, events)
|
|
5
|
+
@config = config
|
|
6
|
+
@api_key = api_key
|
|
7
|
+
@events = events
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def post
|
|
11
|
+
if @events.empty?
|
|
12
|
+
@config.logger.debug('No events to report') if @config.debug_enabled
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
@events.each_slice(@config.events_queue_size) do |event_slice|
|
|
17
|
+
result = post_api(
|
|
18
|
+
"#{@config.events_uri}/events/bulk",
|
|
19
|
+
@config,
|
|
20
|
+
@api_key,
|
|
21
|
+
event_slice.map { |event| formatted_event(event[:e]) },
|
|
22
|
+
'SplitSDKMachineIP' => event_slice[0][:m][:i],
|
|
23
|
+
'SplitSDKMachineName' => event_slice[0][:m][:n],
|
|
24
|
+
'SplitSDKVersion' => event_slice[0][:m][:s]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if (200..299).include? result.status
|
|
28
|
+
@config.logger.debug("Events reported: #{event_slice.size}") if @config.debug_enabled
|
|
29
|
+
else
|
|
30
|
+
@config.logger.error("Unexpected status code while posting events: #{result.status}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def formatted_event(event)
|
|
38
|
+
{
|
|
39
|
+
key: event[:key],
|
|
40
|
+
trafficTypeName: event[:trafficTypeName],
|
|
41
|
+
eventTypeId: event[:eventTypeId],
|
|
42
|
+
value: event[:value].to_f,
|
|
43
|
+
timestamp: event[:timestamp].to_i
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
|
|
3
|
+
module SplitIoClient
|
|
4
|
+
module FaradayMiddleware
|
|
5
|
+
class Gzip < Faraday::Middleware
|
|
6
|
+
ACCEPT_ENCODING = 'Accept-Encoding'.freeze
|
|
7
|
+
CONTENT_ENCODING = 'Content-Encoding'.freeze
|
|
8
|
+
CONTENT_LENGTH = 'Content-Length'.freeze
|
|
9
|
+
SUPPORTED_ENCODINGS = 'gzip,deflate'.freeze
|
|
10
|
+
RUBY_ENCODING = '1.9'.respond_to?(:force_encoding)
|
|
11
|
+
|
|
12
|
+
def call(env)
|
|
13
|
+
env[:request_headers][ACCEPT_ENCODING] ||= SUPPORTED_ENCODINGS
|
|
14
|
+
@app.call(env).on_complete do |response_env|
|
|
15
|
+
case response_env[:response_headers][CONTENT_ENCODING]
|
|
16
|
+
when 'gzip'
|
|
17
|
+
reset_body(response_env, &method(:uncompress_gzip))
|
|
18
|
+
when 'deflate'
|
|
19
|
+
reset_body(response_env, &method(:inflate))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reset_body(env)
|
|
25
|
+
env[:body] = yield(env[:body])
|
|
26
|
+
env[:response_headers].delete(CONTENT_ENCODING)
|
|
27
|
+
env[:response_headers][CONTENT_LENGTH] = env[:body].length
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def uncompress_gzip(body)
|
|
31
|
+
io = StringIO.new(body)
|
|
32
|
+
gzip_reader = if RUBY_ENCODING
|
|
33
|
+
Zlib::GzipReader.new(io, :encoding => 'ASCII-8BIT')
|
|
34
|
+
else
|
|
35
|
+
Zlib::GzipReader.new(io)
|
|
36
|
+
end
|
|
37
|
+
gzip_reader.read
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def inflate(body)
|
|
41
|
+
# Inflate as a DEFLATE (RFC 1950+RFC 1951) stream
|
|
42
|
+
Zlib::Inflate.inflate(body)
|
|
43
|
+
rescue Zlib::DataError
|
|
44
|
+
# Fall back to inflating as a "raw" deflate stream which
|
|
45
|
+
# Microsoft servers return
|
|
46
|
+
inflate = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
|
47
|
+
begin
|
|
48
|
+
inflate.inflate(body)
|
|
49
|
+
ensure
|
|
50
|
+
inflate.close
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
module Api
|
|
3
|
+
class Impressions < Client
|
|
4
|
+
def initialize(api_key, config, impressions)
|
|
5
|
+
@config = config
|
|
6
|
+
@api_key = api_key
|
|
7
|
+
@impressions = impressions
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def post
|
|
11
|
+
if @impressions.empty?
|
|
12
|
+
@config.logger.debug('No impressions to report') if @config.debug_enabled
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
impressions_by_ip.each do |ip, impressions|
|
|
17
|
+
result = post_api("#{@config.events_uri}/testImpressions/bulk", @config, @api_key, impressions, 'SplitSDKMachineIP' => ip)
|
|
18
|
+
|
|
19
|
+
if (200..299).include? result.status
|
|
20
|
+
@config.logger.debug("Impressions reported: #{total_impressions(@impressions)}") if @config.debug_enabled
|
|
21
|
+
else
|
|
22
|
+
@config.logger.error("Unexpected status code while posting impressions: #{result.status}")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def total_impressions(impressions)
|
|
28
|
+
return 0 if impressions.nil?
|
|
29
|
+
|
|
30
|
+
impressions.reduce(0) do |impressions_count, impression|
|
|
31
|
+
impressions_count += impression[:keyImpressions].length
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def impressions_by_ip
|
|
38
|
+
@impressions.group_by { |impression| impression[:ip] }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
module Api
|
|
3
|
+
class Metrics < Client
|
|
4
|
+
def initialize(api_key, config, metrics_repository)
|
|
5
|
+
@config = config
|
|
6
|
+
@api_key = api_key
|
|
7
|
+
@metrics_repository = metrics_repository
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def post
|
|
11
|
+
post_latencies
|
|
12
|
+
post_counts
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def post_latencies
|
|
18
|
+
if @metrics_repository.latencies.empty?
|
|
19
|
+
@config.logger.debug('No latencies to report.') if @config.debug_enabled
|
|
20
|
+
else
|
|
21
|
+
@metrics_repository.latencies.each do |name, latencies|
|
|
22
|
+
metrics_time = { name: name, latencies: latencies }
|
|
23
|
+
|
|
24
|
+
result = post_api("#{@config.events_uri}/metrics/time", @config, @api_key, metrics_time)
|
|
25
|
+
|
|
26
|
+
log_status(result, metrics_time.size)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
@metrics_repository.clear_latencies
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def post_counts
|
|
34
|
+
if @metrics_repository.counts.empty?
|
|
35
|
+
@config.logger.debug('No counts to report.') if @config.debug_enabled
|
|
36
|
+
else
|
|
37
|
+
@metrics_repository.counts.each do |name, count|
|
|
38
|
+
metrics_count = { name: name, delta: count }
|
|
39
|
+
|
|
40
|
+
result = post_api("#{@config.events_uri}/metrics/counter", @config, @api_key, metrics_count)
|
|
41
|
+
|
|
42
|
+
log_status(result, metrics_count.size)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
@metrics_repository.clear_counts
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def log_status(result, info_to_log)
|
|
51
|
+
if result == false
|
|
52
|
+
@config.logger.error("Failed to make a http request")
|
|
53
|
+
elsif (200..299).include? result.status
|
|
54
|
+
@config.logger.debug("Metric time reported: #{info_to_log}") if @config.debug_enabled
|
|
55
|
+
else
|
|
56
|
+
@config.logger.error("Unexpected status code while posting time metrics: #{result.status}")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
module Api
|
|
3
|
+
class Segments < Client
|
|
4
|
+
def initialize(api_key, config, metrics, segments_repository)
|
|
5
|
+
@config = config
|
|
6
|
+
@metrics = metrics
|
|
7
|
+
@api_key = api_key
|
|
8
|
+
@segments_repository = segments_repository
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def store_segments_by_names(names)
|
|
12
|
+
start = Time.now
|
|
13
|
+
prefix = 'segmentChangeFetcher'
|
|
14
|
+
|
|
15
|
+
return if names.nil? || names.empty?
|
|
16
|
+
|
|
17
|
+
names.each do |name|
|
|
18
|
+
since = @segments_repository.get_change_number(name)
|
|
19
|
+
while true
|
|
20
|
+
fetch_segments(name, prefix, since).each { |segment| @segments_repository.add_to_segment(segment) }
|
|
21
|
+
@config.logger.debug("Segment #{name} fetched before: #{since}, till: #{@segments_repository.get_change_number(name)}") if @config.debug_enabled
|
|
22
|
+
|
|
23
|
+
break if (since.to_i >= @segments_repository.get_change_number(name).to_i)
|
|
24
|
+
since = @segments_repository.get_change_number(name)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
latency = (Time.now - start) * 1000.0
|
|
29
|
+
@metrics.time(prefix + '.time', latency)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def fetch_segments(name, prefix, since)
|
|
35
|
+
segments = []
|
|
36
|
+
segment = get_api("#{@config.base_uri}/segmentChanges/#{name}", @config, @api_key, since: since)
|
|
37
|
+
|
|
38
|
+
if segment == false
|
|
39
|
+
@config.logger.error("Failed to make a http request")
|
|
40
|
+
elsif segment.status / 100 == 2
|
|
41
|
+
segment_content = JSON.parse(segment.body, symbolize_names: true)
|
|
42
|
+
@segments_repository.set_change_number(name, segment_content[:till])
|
|
43
|
+
@metrics.count(prefix + '.status.' + segment.status.to_s, 1)
|
|
44
|
+
|
|
45
|
+
if @config.debug_enabled
|
|
46
|
+
@config.logger.debug("\'#{segment_content[:name]}\' segment retrieved.")
|
|
47
|
+
@config.logger.debug("\'#{segment_content[:name]}\' #{segment_content[:added].size} added keys") if segment_content[:added].size > 0
|
|
48
|
+
@config.logger.debug("\'#{segment_content[:name]}\' #{segment_content[:removed].size} removed keys") if segment_content[:removed].size > 0
|
|
49
|
+
end
|
|
50
|
+
@config.logger.debug("#{segment_content}") if @config.transport_debug_enabled
|
|
51
|
+
|
|
52
|
+
segments << segment_content
|
|
53
|
+
else
|
|
54
|
+
@config.logger.error("Unexpected result from API Call for Segment #{name} status #{segment.status.to_s} since #{since.to_s}")
|
|
55
|
+
@metrics.count(prefix + '.status.' + segment.status.to_s, 1)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
segments
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
module Api
|
|
3
|
+
class Splits < Client
|
|
4
|
+
def initialize(api_key, config, metrics)
|
|
5
|
+
@api_key = api_key
|
|
6
|
+
@config = config
|
|
7
|
+
@metrics = metrics
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def since(since)
|
|
11
|
+
start = Time.now
|
|
12
|
+
prefix = 'splitChangeFetcher'
|
|
13
|
+
splits = get_api("#{@config.base_uri}/splitChanges", @config, @api_key, since: since)
|
|
14
|
+
|
|
15
|
+
if splits == false
|
|
16
|
+
@config.logger.error("Failed to make a http request")
|
|
17
|
+
elsif splits.status / 100 == 2
|
|
18
|
+
result = splits_with_segment_names(splits.body)
|
|
19
|
+
|
|
20
|
+
@metrics.count(prefix + '.status.' + splits.status.to_s, 1)
|
|
21
|
+
|
|
22
|
+
@config.logger.debug("#{result[:splits].length} splits retrieved. since=#{since}") if @config.debug_enabled and result[:splits].length > 0
|
|
23
|
+
@config.logger.debug("#{result}") if @config.transport_debug_enabled
|
|
24
|
+
else
|
|
25
|
+
@metrics.count(prefix + '.status.' + splits.status.to_s, 1)
|
|
26
|
+
|
|
27
|
+
@config.logger.error('Unexpected result from Splits API call')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
latency = (Time.now - start) * 1000.0
|
|
31
|
+
@metrics.time(prefix + '.time', latency)
|
|
32
|
+
|
|
33
|
+
result
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def splits_with_segment_names(splits_json)
|
|
39
|
+
parsed_splits = JSON.parse(splits_json, symbolize_names: true)
|
|
40
|
+
|
|
41
|
+
parsed_splits[:segment_names] =
|
|
42
|
+
parsed_splits[:splits].each_with_object(Set.new) do |split, splits|
|
|
43
|
+
splits << segment_names(split)
|
|
44
|
+
end.flatten
|
|
45
|
+
|
|
46
|
+
parsed_splits
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def segment_names(split)
|
|
50
|
+
split[:conditions].each_with_object(Set.new) do |condition, names|
|
|
51
|
+
condition[:matcherGroup][:matchers].each do |matcher|
|
|
52
|
+
next if matcher[:userDefinedSegmentMatcherData].nil?
|
|
53
|
+
|
|
54
|
+
names << matcher[:userDefinedSegmentMatcherData][:segmentName]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
def initialize
|
|
8
|
+
@murmur_hash = case RUBY_PLATFORM
|
|
9
|
+
when 'java'
|
|
10
|
+
Proc.new { |key, seed| Java::MurmurHash3.murmurhash3_x86_32(key, seed) }
|
|
11
|
+
else
|
|
12
|
+
Proc.new { |key, seed| Digest::MurmurHashMRI3_x86_32.rawdigest(key, [seed].pack('L')) }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
#
|
|
17
|
+
# Checks if the partiotion size is 100%
|
|
18
|
+
#
|
|
19
|
+
# @param partitions [object] array of partitions
|
|
20
|
+
#
|
|
21
|
+
# @return [boolean] true if partition is 100% false otherwise
|
|
22
|
+
def hundred_percent_one_treatment?(partitions)
|
|
23
|
+
(partitions.size != 1) ? false : (partitions.first.size == 100)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# gets the appropriate treatment based on id, seed and partition value
|
|
28
|
+
#
|
|
29
|
+
# @param id [string] user key
|
|
30
|
+
# @param seed [number] seed for the user key
|
|
31
|
+
# @param partitions [object] array of partitions
|
|
32
|
+
#
|
|
33
|
+
# @return traetment [object] treatment value
|
|
34
|
+
def get_treatment(id, seed, partitions, legacy_algo)
|
|
35
|
+
legacy = [1, nil].include?(legacy_algo)
|
|
36
|
+
|
|
37
|
+
if partitions.empty?
|
|
38
|
+
return SplitIoClient::Engine::Models::Treatment::CONTROL
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if hundred_percent_one_treatment?(partitions)
|
|
42
|
+
return (partitions.first).treatment
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
return get_treatment_for_key(bucket(count_hash(id, seed, legacy_algo)), partitions)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# returns a hash value for the give key, seed pair
|
|
49
|
+
#
|
|
50
|
+
# @param key [String] user key
|
|
51
|
+
# @param seed [Fixnum] seed for the user key
|
|
52
|
+
#
|
|
53
|
+
# @return hash [String] hash value
|
|
54
|
+
def count_hash(key, seed, legacy)
|
|
55
|
+
legacy ? legacy_hash(key, seed) : murmur_hash(key, seed)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def murmur_hash(key, seed)
|
|
59
|
+
@murmur_hash.call(key, seed)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def legacy_hash(key, seed)
|
|
63
|
+
h = 0
|
|
64
|
+
|
|
65
|
+
for i in 0..key.length-1
|
|
66
|
+
h = to_int32(31 * h + key[i].ord)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
h^seed
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#
|
|
73
|
+
# misc method to convert ruby number to int 32 since overflow is handled different to java
|
|
74
|
+
#
|
|
75
|
+
# @param number [number] ruby number value
|
|
76
|
+
#
|
|
77
|
+
# @return [int] returns the int 32 value of the provided number
|
|
78
|
+
def to_int32(number)
|
|
79
|
+
begin
|
|
80
|
+
sign = number < 0 ? -1 : 1
|
|
81
|
+
abs = number.abs
|
|
82
|
+
return 0 if abs == 0 || abs == Float::INFINITY
|
|
83
|
+
rescue
|
|
84
|
+
return 0
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
pos_int = sign * abs.floor
|
|
88
|
+
int_32bit = pos_int % 2**32
|
|
89
|
+
|
|
90
|
+
return int_32bit - 2**32 if int_32bit >= 2**31
|
|
91
|
+
int_32bit
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
#
|
|
95
|
+
# returns the treatment for a bucket given the partitions
|
|
96
|
+
#
|
|
97
|
+
# @param bucket [number] bucket value
|
|
98
|
+
# @param parittions [object] array of partitions
|
|
99
|
+
#
|
|
100
|
+
# @return treatment [treatment] treatment value for this bucket and partitions
|
|
101
|
+
def get_treatment_for_key(bucket, partitions)
|
|
102
|
+
buckets_covered_thus_far = 0
|
|
103
|
+
partitions.each do |p|
|
|
104
|
+
unless p.is_empty?
|
|
105
|
+
buckets_covered_thus_far += p.size
|
|
106
|
+
return p.treatment if buckets_covered_thus_far >= bucket
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
return SplitIoClient::Engine::Models::Treatment::CONTROL
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
#
|
|
114
|
+
# returns bucket value for the given hash value
|
|
115
|
+
#
|
|
116
|
+
# @param hash_value [string] hash value
|
|
117
|
+
#
|
|
118
|
+
# @return bucket [number] bucket number
|
|
119
|
+
def bucket(hash_value)
|
|
120
|
+
(hash_value.abs % 100) + 1
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|