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,49 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
module Cache
|
|
3
|
+
module Senders
|
|
4
|
+
class MetricsSender
|
|
5
|
+
def initialize(metrics_repository, config, api_key)
|
|
6
|
+
@metrics_repository = metrics_repository
|
|
7
|
+
@config = config
|
|
8
|
+
@api_key = api_key
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
return if ENV['SPLITCLIENT_ENV'] == 'test'
|
|
13
|
+
|
|
14
|
+
metrics_thread
|
|
15
|
+
|
|
16
|
+
if defined?(PhusionPassenger)
|
|
17
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
|
18
|
+
metrics_thread if forked
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def metrics_thread
|
|
26
|
+
@config.threads[:metrics_sender] = Thread.new do
|
|
27
|
+
@config.logger.info('Starting metrics service')
|
|
28
|
+
|
|
29
|
+
loop do
|
|
30
|
+
post_metrics
|
|
31
|
+
|
|
32
|
+
sleep(SplitIoClient::Utilities.randomize_interval(@config.metrics_refresh_rate))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def post_metrics
|
|
38
|
+
metrics_client.post
|
|
39
|
+
rescue StandardError => error
|
|
40
|
+
@config.log_found_exception(__method__.to_s, error)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def metrics_client
|
|
44
|
+
SplitIoClient::Api::Metrics.new(@api_key, @config, @metrics_repository)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
require 'timeout'
|
|
3
|
+
|
|
4
|
+
module SplitIoClient
|
|
5
|
+
module Cache
|
|
6
|
+
module Stores
|
|
7
|
+
class SDKBlocker
|
|
8
|
+
attr_reader :splits_repository
|
|
9
|
+
attr_writer :splits_thread, :segments_thread
|
|
10
|
+
|
|
11
|
+
def initialize(config, splits_repository, segments_repository)
|
|
12
|
+
@config = config
|
|
13
|
+
@splits_repository = splits_repository
|
|
14
|
+
@segments_repository = segments_repository
|
|
15
|
+
|
|
16
|
+
@splits_repository.not_ready!
|
|
17
|
+
@segments_repository.not_ready!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def splits_ready!
|
|
21
|
+
@splits_repository.ready!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def segments_ready!
|
|
25
|
+
@segments_repository.ready!
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def block
|
|
29
|
+
begin
|
|
30
|
+
Timeout::timeout(@config.block_until_ready) do
|
|
31
|
+
sleep 0.1 until ready?
|
|
32
|
+
end
|
|
33
|
+
rescue Timeout::Error
|
|
34
|
+
fail SDKBlockerTimeoutExpiredException, 'SDK start up timeout expired'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@config.logger.info('SplitIO SDK is ready')
|
|
38
|
+
@splits_thread.run
|
|
39
|
+
@segments_thread.run
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ready?
|
|
43
|
+
@splits_repository.ready? && @segments_repository.ready?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
module Cache
|
|
3
|
+
module Stores
|
|
4
|
+
class SegmentStore
|
|
5
|
+
attr_reader :segments_repository
|
|
6
|
+
|
|
7
|
+
def initialize(segments_repository, config, api_key, metrics, sdk_blocker = nil)
|
|
8
|
+
@segments_repository = segments_repository
|
|
9
|
+
@config = config
|
|
10
|
+
@api_key = api_key
|
|
11
|
+
@metrics = metrics
|
|
12
|
+
@sdk_blocker = sdk_blocker
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
if ENV['SPLITCLIENT_ENV'] == 'test'
|
|
17
|
+
store_segments
|
|
18
|
+
else
|
|
19
|
+
segments_thread
|
|
20
|
+
|
|
21
|
+
if defined?(PhusionPassenger)
|
|
22
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
|
23
|
+
segments_thread if forked
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def segments_thread
|
|
32
|
+
@config.threads[:segment_store] = @sdk_blocker.segments_thread = Thread.new do
|
|
33
|
+
@config.logger.info('Starting segments fetcher service')
|
|
34
|
+
@config.block_until_ready > 0 ? blocked_store : unblocked_store
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def blocked_store
|
|
39
|
+
loop do
|
|
40
|
+
next unless @sdk_blocker.splits_repository.ready?
|
|
41
|
+
|
|
42
|
+
store_segments
|
|
43
|
+
@config.logger.debug("Segment names: #{@segments_repository.used_segment_names.to_a}") if @config.debug_enabled
|
|
44
|
+
|
|
45
|
+
unless @sdk_blocker.ready?
|
|
46
|
+
@sdk_blocker.segments_ready!
|
|
47
|
+
@config.logger.info('segments are ready')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sleep_for = random_interval(@config.segments_refresh_rate)
|
|
51
|
+
@config.logger.debug("Segments store is sleeping for: #{sleep_for} seconds") if @config.debug_enabled
|
|
52
|
+
sleep(sleep_for)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def unblocked_store
|
|
57
|
+
loop do
|
|
58
|
+
store_segments
|
|
59
|
+
|
|
60
|
+
sleep(random_interval(@config.segments_refresh_rate))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def store_segments
|
|
65
|
+
segments_api.store_segments_by_names(@segments_repository.used_segment_names)
|
|
66
|
+
rescue StandardError => error
|
|
67
|
+
@config.log_found_exception(__method__.to_s, error)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def random_interval(interval)
|
|
71
|
+
random_factor = Random.new.rand(50..100) / 100.0
|
|
72
|
+
|
|
73
|
+
interval * random_factor
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def segments_api
|
|
77
|
+
SplitIoClient::Api::Segments.new(@api_key, @config, @metrics, @segments_repository)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
module Cache
|
|
3
|
+
module Stores
|
|
4
|
+
class SplitStore
|
|
5
|
+
attr_reader :splits_repository
|
|
6
|
+
|
|
7
|
+
def initialize(splits_repository, config, api_key, metrics, sdk_blocker = nil)
|
|
8
|
+
@splits_repository = splits_repository
|
|
9
|
+
@config = config
|
|
10
|
+
@api_key = api_key
|
|
11
|
+
@metrics = metrics
|
|
12
|
+
@sdk_blocker = sdk_blocker
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
if ENV['SPLITCLIENT_ENV'] == 'test'
|
|
17
|
+
store_splits
|
|
18
|
+
else
|
|
19
|
+
splits_thread
|
|
20
|
+
|
|
21
|
+
if defined?(PhusionPassenger)
|
|
22
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
|
23
|
+
splits_thread if forked
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def splits_thread
|
|
32
|
+
@config.threads[:split_store] = @sdk_blocker.splits_thread = Thread.new do
|
|
33
|
+
@config.logger.info('Starting splits fetcher service')
|
|
34
|
+
loop do
|
|
35
|
+
store_splits
|
|
36
|
+
|
|
37
|
+
sleep(random_interval(@config.features_refresh_rate))
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def store_splits
|
|
43
|
+
data = splits_since(@splits_repository.get_change_number)
|
|
44
|
+
|
|
45
|
+
data[:splits] && data[:splits].each do |split|
|
|
46
|
+
add_split_unless_archived(split)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@splits_repository.set_segment_names(data[:segment_names])
|
|
50
|
+
@splits_repository.set_change_number(data[:till])
|
|
51
|
+
|
|
52
|
+
@config.logger.debug("segments seen(#{data[:segment_names].length}): #{data[:segment_names].to_a}") if @config.debug_enabled
|
|
53
|
+
|
|
54
|
+
if @config.block_until_ready > 0 && !@sdk_blocker.ready?
|
|
55
|
+
@sdk_blocker.splits_ready!
|
|
56
|
+
@config.logger.info('splits are ready')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
rescue StandardError => error
|
|
60
|
+
@config.log_found_exception(__method__.to_s, error)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def random_interval(interval)
|
|
64
|
+
random_factor = Random.new.rand(50..100) / 100.0
|
|
65
|
+
|
|
66
|
+
interval * random_factor
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def splits_since(since)
|
|
70
|
+
SplitIoClient::Api::Splits.new(@api_key, @config, @metrics).since(since)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def add_split_unless_archived(split)
|
|
74
|
+
if Engine::Models::Split.archived?(split)
|
|
75
|
+
@config.logger.debug("Seeing archived split #{split[:name]}") if @config.debug_enabled
|
|
76
|
+
|
|
77
|
+
remove_archived_split(split)
|
|
78
|
+
else
|
|
79
|
+
store_split(split)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def remove_archived_split(split)
|
|
84
|
+
@config.logger.debug("removing split from store(#{split})") if @config.debug_enabled
|
|
85
|
+
|
|
86
|
+
@splits_repository.remove_split(split[:name])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def store_split(split)
|
|
90
|
+
@config.logger.debug("storing split (#{split[:name]})") if @config.debug_enabled
|
|
91
|
+
|
|
92
|
+
@splits_repository.add_split(split)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
class LocalhostSplitClient
|
|
3
|
+
include SplitIoClient::LocalhostUtils
|
|
4
|
+
|
|
5
|
+
#
|
|
6
|
+
# variables to if the sdk is being used in localhost mode and store the list of features
|
|
7
|
+
attr_reader :localhost_mode
|
|
8
|
+
attr_reader :localhost_mode_features
|
|
9
|
+
|
|
10
|
+
#
|
|
11
|
+
# Creates a new split client instance that reads from the given splits file
|
|
12
|
+
#
|
|
13
|
+
# @param splits_file [File] file that contains some splits
|
|
14
|
+
#
|
|
15
|
+
# @return [LocalhostSplitIoClient] split.io localhost client instance
|
|
16
|
+
def initialize(splits_file, reload_rate = nil)
|
|
17
|
+
@localhost_mode = true
|
|
18
|
+
@localhost_mode_features = []
|
|
19
|
+
load_localhost_mode_features(splits_file, reload_rate)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# method that returns the sdk gem version
|
|
24
|
+
#
|
|
25
|
+
# @return [string] version value for this sdk
|
|
26
|
+
def self.sdk_version
|
|
27
|
+
'ruby-'+SplitIoClient::VERSION
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get_treatments(key, split_names, attributes = nil)
|
|
31
|
+
split_names.each_with_object({}) do |name, memo|
|
|
32
|
+
puts "name #{name} memo #{memo}"
|
|
33
|
+
memo.merge!(name => get_treatment(key, name, attributes))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
#
|
|
38
|
+
# obtains the treatment for a given feature
|
|
39
|
+
#
|
|
40
|
+
# @param id [string] user id
|
|
41
|
+
# @param feature [string] name of the feature that is being validated
|
|
42
|
+
#
|
|
43
|
+
# @return [Treatment] treatment constant value
|
|
44
|
+
def get_treatment(id, feature, attributes = nil)
|
|
45
|
+
unless id
|
|
46
|
+
@config.logger.warn('id was null for feature: ' + feature)
|
|
47
|
+
return SplitIoClient::Engine::Models::Treatment::CONTROL
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
unless feature
|
|
51
|
+
@config.logger.warn('feature was null for id: ' + id)
|
|
52
|
+
return SplitIoClient::Engine::Models::Treatment::CONTROL
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
result = get_localhost_treatment(feature)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def track
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
#
|
|
64
|
+
# auxiliary method to get the treatments avoding exceptions
|
|
65
|
+
#
|
|
66
|
+
# @param id [string] user id
|
|
67
|
+
# @param feature [string] name of the feature that is being validated
|
|
68
|
+
#
|
|
69
|
+
# @return [Treatment] tretment constant value
|
|
70
|
+
def get_treatment_without_exception_handling(id, feature, attributes = nil)
|
|
71
|
+
get_treatment(id, feature, attributes)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
#
|
|
75
|
+
# method to check if the sdk is running in localhost mode based on api key
|
|
76
|
+
#
|
|
77
|
+
# @return [boolean] True if is in localhost mode, false otherwise
|
|
78
|
+
def is_localhost_mode?
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
#
|
|
83
|
+
# method to check the treatment for the given feature in localhost mode
|
|
84
|
+
#
|
|
85
|
+
# @return [boolean] true if the feature is available in localhost mode, false otherwise
|
|
86
|
+
def get_localhost_treatment(feature)
|
|
87
|
+
treatment = @localhost_mode_features.select { |h| h[:feature] == feature }.last || {}
|
|
88
|
+
|
|
89
|
+
treatment[:treatment] || SplitIoClient::Engine::Models::Treatment::CONTROL
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
module SplitIoClient
|
|
2
|
+
class SplitClient
|
|
3
|
+
#
|
|
4
|
+
# Creates a new split client instance that connects to split.io API.
|
|
5
|
+
#
|
|
6
|
+
# @param api_key [String] the API key for your split account
|
|
7
|
+
#
|
|
8
|
+
# @return [SplitIoClient] split.io client instance
|
|
9
|
+
def initialize(api_key, config = {}, adapter = nil, splits_repository, segments_repository, impressions_repository, metrics_repository, events_repository)
|
|
10
|
+
@config = config
|
|
11
|
+
|
|
12
|
+
@splits_repository = splits_repository
|
|
13
|
+
@segments_repository = segments_repository
|
|
14
|
+
@impressions_repository = impressions_repository
|
|
15
|
+
@metrics_repository = metrics_repository
|
|
16
|
+
@events_repository = events_repository
|
|
17
|
+
|
|
18
|
+
@adapter = adapter
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def get_treatments(key, split_names, attributes = nil)
|
|
22
|
+
bucketing_key, matching_key = keys_from_key(key)
|
|
23
|
+
evaluator = Engine::Parser::Evaluator.new(@segments_repository, @splits_repository, true)
|
|
24
|
+
|
|
25
|
+
treatments_labels_change_numbers =
|
|
26
|
+
@splits_repository.get_splits(split_names).each_with_object({}) do |(name, data), memo|
|
|
27
|
+
memo.merge!(name => get_treatment(key, name, attributes, data, false, true, evaluator))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if @config.impressions_queue_size > 0
|
|
31
|
+
time = (Time.now.to_f * 1000.0).to_i
|
|
32
|
+
@impressions_repository.add_bulk(
|
|
33
|
+
matching_key, bucketing_key, treatments_labels_change_numbers, time
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
route_impressions(split_names, matching_key, bucketing_key, time, treatments_labels_change_numbers, attributes)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
split_names = treatments_labels_change_numbers.keys
|
|
40
|
+
treatments = treatments_labels_change_numbers.values.map { |v| v[:treatment] }
|
|
41
|
+
|
|
42
|
+
Hash[split_names.zip(treatments)]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# obtains the treatment for a given feature
|
|
47
|
+
#
|
|
48
|
+
# @param key [String/Hash] user id or hash with matching_key/bucketing_key
|
|
49
|
+
# @param split_name [String/Array] name of the feature that is being validated or array of them
|
|
50
|
+
# @param attributes [Hash] attributes to pass to the treatment class
|
|
51
|
+
# @param split_data [Hash] split data, when provided this method doesn't fetch splits_repository for the data
|
|
52
|
+
# @param store_impressions [Boolean] impressions aren't stored if this flag is false
|
|
53
|
+
# @param multiple [Hash] internal flag to signal if method is called by get_treatments
|
|
54
|
+
# @param evaluator [Evaluator] Evaluator class instance, used to cache treatments
|
|
55
|
+
#
|
|
56
|
+
# @return [String/Hash] Treatment as String or Hash of treatments in case of array of features
|
|
57
|
+
def get_treatment(
|
|
58
|
+
key, split_name, attributes = nil, split_data = nil, store_impressions = true,
|
|
59
|
+
multiple = false, evaluator = nil
|
|
60
|
+
)
|
|
61
|
+
bucketing_key, matching_key = keys_from_key(key)
|
|
62
|
+
treatment_data = { label: Engine::Models::Label::DEFINITION_NOT_FOUND, treatment: SplitIoClient::Engine::Models::Treatment::CONTROL }
|
|
63
|
+
evaluator ||= Engine::Parser::Evaluator.new(@segments_repository, @splits_repository)
|
|
64
|
+
|
|
65
|
+
if matching_key.nil?
|
|
66
|
+
@config.logger.warn('matching_key was null for split_name: ' + split_name.to_s)
|
|
67
|
+
return parsed_treatment(multiple, treatment_data)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if split_name.nil?
|
|
71
|
+
@config.logger.warn('split_name was null for key: ' + key)
|
|
72
|
+
return parsed_treatment(multiple, treatment_data)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
start = Time.now
|
|
76
|
+
|
|
77
|
+
begin
|
|
78
|
+
split = multiple ? split_data : @splits_repository.get_split(split_name)
|
|
79
|
+
|
|
80
|
+
if split.nil?
|
|
81
|
+
@config.logger.debug("split_name: #{split_name} does not exist. Returning CONTROL")
|
|
82
|
+
return parsed_treatment(multiple, treatment_data)
|
|
83
|
+
else
|
|
84
|
+
treatment_data =
|
|
85
|
+
evaluator.call(
|
|
86
|
+
{ bucketing_key: bucketing_key, matching_key: matching_key }, split, attributes
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
rescue StandardError => error
|
|
90
|
+
@config.log_found_exception(__method__.to_s, error)
|
|
91
|
+
|
|
92
|
+
store_impression(
|
|
93
|
+
split_name, matching_key, bucketing_key,
|
|
94
|
+
{
|
|
95
|
+
treatment: SplitIoClient::Engine::Models::Treatment::CONTROL,
|
|
96
|
+
label: SplitIoClient::Engine::Models::Label::EXCEPTION
|
|
97
|
+
},
|
|
98
|
+
store_impressions, attributes
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return parsed_treatment(multiple, treatment_data)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
begin
|
|
105
|
+
latency = (Time.now - start) * 1000.0
|
|
106
|
+
# Disable impressions if @config.impressions_queue_size == -1
|
|
107
|
+
split && store_impression(split_name, matching_key, bucketing_key, treatment_data, store_impressions, attributes)
|
|
108
|
+
|
|
109
|
+
# Measure
|
|
110
|
+
@adapter.metrics.time('sdk.get_treatment', latency)
|
|
111
|
+
rescue StandardError => error
|
|
112
|
+
@config.log_found_exception(__method__.to_s, error)
|
|
113
|
+
|
|
114
|
+
store_impression(
|
|
115
|
+
split_name, matching_key, bucketing_key,
|
|
116
|
+
{
|
|
117
|
+
treatment: SplitIoClient::Engine::Models::Treatment::CONTROL,
|
|
118
|
+
label: SplitIoClient::Engine::Models::Label::EXCEPTION
|
|
119
|
+
},
|
|
120
|
+
store_impressions, attributes
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return parsed_treatment(multiple, treatment_data)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
parsed_treatment(multiple, treatment_data)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def destroy
|
|
130
|
+
@config.logger.info('Split client shutdown started...') if @config.debug_enabled
|
|
131
|
+
|
|
132
|
+
@config.threads[:impressions_sender].raise(SplitIoClient::ImpressionShutdownException)
|
|
133
|
+
@config.threads.reject { |k, _| k == :impressions_sender }.each do |name, thread|
|
|
134
|
+
Thread.kill(thread)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
@metrics_repository.clear
|
|
138
|
+
@splits_repository.clear
|
|
139
|
+
@segments_repository.clear
|
|
140
|
+
@events_repository.clear
|
|
141
|
+
|
|
142
|
+
@config.logger.info('Split client shutdown complete') if @config.debug_enabled
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def store_impression(split_name, matching_key, bucketing_key, treatment, store_impressions, attributes)
|
|
146
|
+
time = (Time.now.to_f * 1000.0).to_i
|
|
147
|
+
route_impression(split_name, matching_key, bucketing_key, time, treatment, attributes) if @config.impression_listener && store_impressions
|
|
148
|
+
|
|
149
|
+
return if @config.impressions_queue_size <= 0 || !store_impressions
|
|
150
|
+
|
|
151
|
+
@impressions_repository.add(split_name,
|
|
152
|
+
'keyName' => matching_key,
|
|
153
|
+
'bucketingKey' => bucketing_key,
|
|
154
|
+
'treatment' => treatment[:treatment],
|
|
155
|
+
'label' => @config.labels_enabled ? treatment[:label] : nil,
|
|
156
|
+
'time' => time,
|
|
157
|
+
'changeNumber' => treatment[:change_number]
|
|
158
|
+
)
|
|
159
|
+
rescue StandardError => error
|
|
160
|
+
@config.log_found_exception(__method__.to_s, error)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def route_impression(split_name, matching_key, bucketing_key, time, treatment, attributes)
|
|
164
|
+
impression_router.add(
|
|
165
|
+
split_name: split_name,
|
|
166
|
+
matching_key: matching_key,
|
|
167
|
+
bucketing_key: bucketing_key,
|
|
168
|
+
time: time,
|
|
169
|
+
treatment: treatment,
|
|
170
|
+
attributes: attributes
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def route_impressions(split_names, matching_key, bucketing_key, time, treatments_labels_change_numbers, attributes)
|
|
175
|
+
impression_router.add_bulk(
|
|
176
|
+
split_names: split_names,
|
|
177
|
+
matching_key: matching_key,
|
|
178
|
+
bucketing_key: bucketing_key,
|
|
179
|
+
time: time,
|
|
180
|
+
treatments_labels_change_numbers: treatments_labels_change_numbers,
|
|
181
|
+
attributes: attributes
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def impression_router
|
|
186
|
+
@impression_router ||= SplitIoClient::ImpressionRouter.new(@config)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def track(key, traffic_type, event_type, value = nil)
|
|
190
|
+
@events_repository.add(key, traffic_type, event_type, (Time.now.to_f * 1000).to_i, value)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def keys_from_key(key)
|
|
194
|
+
case key.class.to_s
|
|
195
|
+
when 'Hash'
|
|
196
|
+
key.values_at(:bucketing_key, :matching_key).map { |k| k.nil? ? nil : k.to_s }
|
|
197
|
+
else
|
|
198
|
+
[nil, key].map { |k| k.nil? ? nil : k.to_s }
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def parsed_treatment(multiple, treatment_data)
|
|
203
|
+
if multiple
|
|
204
|
+
{
|
|
205
|
+
treatment: treatment_data[:treatment],
|
|
206
|
+
label: treatment_data[:label],
|
|
207
|
+
change_number: treatment_data[:change_number]
|
|
208
|
+
}
|
|
209
|
+
else
|
|
210
|
+
treatment_data[:treatment]
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|