splitclient-rb 4.5.1-java
Sign up to get free protection for your applications and to get access to all the features.
- 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
|