splitclient-rb 7.3.4 → 7.3.5.pre.rc3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23b9c717284c495997eb1a3808850bd25816c29a447f13147f51a0c60138187e
4
- data.tar.gz: 5b438d7782da8fb3e67689161d5fb53083665a5e4703f1b5fe628718e3cde81d
3
+ metadata.gz: df863c319448efe787511c74dcf0943f7f0678a1307d2dd97c3da68bab54091b
4
+ data.tar.gz: e36cb5e66fbc19cffb7343e8577436064713a79362bfc2f899f9f5d3cf9827fc
5
5
  SHA512:
6
- metadata.gz: 8dfa68a91b4e0cff3090d96640494f8755515499699c882b073ebe292f6920386e39b1c0be1b24a52b581f74d7092faa3c9ad5bf33bdf8565e7576d3f2ff2669
7
- data.tar.gz: 76c004e32ac5e215b7e45690a96e9f0b9a2735f1212004f76c4402e3fabcbab2891ad22aa4caba3b6fb8aa5270b9a397589851a4d576254e7b0d70d01e78b6b7
6
+ metadata.gz: a7ac1c8ea998d34c709d71b3725fcac423056cc544a6d84eabdd4a86552d3dfa023b0b8bc253a0f9825a47f2082a165ba2887078225e9cc661f7b2d1204c943c
7
+ data.tar.gz: 9b96aeb5547fe3debf7b53a05f64d7fbe4c5e83de4ea0a4a96cbd40d6e7cafe73cc5a9b45ad32246edbf73faa9f6f83526631b1ba2c7f691a4597601b90d4bb1
data/.rubocop.yml CHANGED
@@ -26,7 +26,7 @@ Metrics/ParameterLists:
26
26
  - lib/splitclient-rb/engine/sync_manager.rb
27
27
 
28
28
  Metrics/LineLength:
29
- Max: 130
29
+ Max: 135
30
30
  Exclude:
31
31
  - spec/sse/**/*
32
32
  - spec/integrations/**/*
@@ -35,6 +35,7 @@ Metrics/LineLength:
35
35
  - spec/telemetry/synchronizer_spec.rb
36
36
  - spec/splitclient/split_config_spec.rb
37
37
  - spec/engine/push_manager_spec.rb
38
+ - spec/cache/senders/impressions_sender_adapter_spec.rb
38
39
 
39
40
  Style/BracesAroundHashParameters:
40
41
  Exclude:
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bitarray'
4
+
5
+ module SplitIoClient
6
+ module Cache
7
+ module Filter
8
+ class BloomFilter
9
+ def initialize(capacity, false_positive_probability = 0.001)
10
+ @capacity = capacity.round
11
+ m = best_m(capacity, false_positive_probability)
12
+ @ba = BitArray.new(m.round)
13
+ @k = best_k(capacity)
14
+ end
15
+
16
+ def add(string)
17
+ return false if contains?(string)
18
+
19
+ positions = hashes(string)
20
+
21
+ positions.each { |position| @ba[position] = 1 }
22
+
23
+ true
24
+ end
25
+
26
+ def contains?(string)
27
+ !hashes(string).any? { |ea| @ba[ea] == 0 }
28
+ end
29
+
30
+ def clear
31
+ @ba.size.times { |i| @ba[i] = 0 }
32
+ end
33
+
34
+ private
35
+
36
+ # m is the required number of bits in the array
37
+ def best_m(capacity, false_positive_probability)
38
+ -(capacity * Math.log(false_positive_probability)) / (Math.log(2) ** 2)
39
+ end
40
+
41
+ # k is the number of hash functions that minimizes the probability of false positives
42
+ def best_k(capacity)
43
+ (Math.log(2) * (@ba.size / capacity)).round
44
+ end
45
+
46
+ def hashes(data)
47
+ m = @ba.size
48
+ h = Digest::MD5.hexdigest(data.to_s).to_i(16)
49
+ x = h % m
50
+ h /= m
51
+ y = h % m
52
+ h /= m
53
+ z = h % m
54
+ [x] + 1.upto(@k - 1).collect do |i|
55
+ x = (x + y) % m
56
+ y = (y + z) % m
57
+ x
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Cache
5
+ module Filter
6
+ class FilterAdapter
7
+ def initialize(config, filter)
8
+ @config = config
9
+ @filter = filter
10
+ end
11
+
12
+ def add(feature_name, key)
13
+ @filter.add("#{feature_name}#{key}")
14
+ rescue StandardError => e
15
+ @config.log_found_exception(__method__.to_s, e)
16
+ end
17
+
18
+ def contains?(feature_name, key)
19
+ @filter.contains?("#{feature_name}#{key}")
20
+ rescue StandardError => e
21
+ @config.log_found_exception(__method__.to_s, e)
22
+ end
23
+
24
+ def clear
25
+ @filter.clear
26
+ rescue StandardError => e
27
+ @config.log_found_exception(__method__.to_s, e)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,10 @@
1
+ module SplitIoClient
2
+ module Observers
3
+ class NoopImpressionObserver
4
+ def test_and_set(impression)
5
+ # no-op
6
+ end
7
+ end
8
+ end
9
+ end
10
+
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Cache
5
+ module Senders
6
+ class MemoryImpressionsSender < ImpressionsSenderAdapter
7
+ def initialize(config, telemetry_api, impressions_api)
8
+ @config = config
9
+ @telemetry_api = telemetry_api
10
+ @impressions_api = impressions_api
11
+ end
12
+
13
+ def record_uniques_key(uniques)
14
+ uniques_keys = uniques_formatter(uniques)
15
+
16
+ @telemetry_api.record_unique_keys(uniques_keys) unless uniques_keys.nil?
17
+ rescue StandardError => e
18
+ @config.log_found_exception(__method__.to_s, e)
19
+ end
20
+
21
+ def record_impressions_count(impressions_count)
22
+ counts = impressions_count_formatter(impressions_count)
23
+
24
+ @impressions_api.post_count(counts) unless counts.nil?
25
+ rescue StandardError => e
26
+ @config.log_found_exception(__method__.to_s, e)
27
+ end
28
+
29
+ private
30
+
31
+ def uniques_formatter(uniques)
32
+ return if uniques.nil? || uniques.empty?
33
+
34
+ to_return = { mtks: [] }
35
+ uniques.each do |key, value|
36
+ to_return[:mtks] << {
37
+ f: key,
38
+ ks: value.to_a
39
+ }
40
+ end
41
+
42
+ to_return
43
+ rescue StandardError => error
44
+ @config.log_found_exception(__method__.to_s, error)
45
+ nil
46
+ end
47
+
48
+ def impressions_count_formatter(counts)
49
+ return if counts.nil? || counts.empty?
50
+
51
+ formated_counts = {pf: []}
52
+
53
+ counts.each do |key, value|
54
+ key_splited = key.split('::')
55
+
56
+ formated_counts[:pf] << {
57
+ f: key_splited[0].to_s, # feature name
58
+ m: key_splited[1].to_i, # time frame
59
+ rc: value # count
60
+ }
61
+ end
62
+
63
+ formated_counts
64
+ rescue StandardError => error
65
+ @config.log_found_exception(__method__.to_s, error)
66
+ nil
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Cache
5
+ module Senders
6
+ class RedisImpressionsSender < ImpressionsSenderAdapter
7
+ EXPIRE_SECONDS = 3600
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ @adapter = @config.impressions_adapter
12
+ end
13
+
14
+ def record_uniques_key(uniques)
15
+ formatted = uniques_formatter(uniques)
16
+
17
+ unless formatted.nil?
18
+ size = @adapter.add_to_queue(unique_keys_key, formatted)
19
+ @adapter.expire(unique_keys_key, EXPIRE_SECONDS) if formatted.size == size
20
+ end
21
+ rescue StandardError => e
22
+ @config.log_found_exception(__method__.to_s, e)
23
+ end
24
+
25
+ def record_impressions_count(impressions_count)
26
+ return if impressions_count.nil? || impressions_count.empty?
27
+
28
+ result = @adapter.redis.pipelined do |pipeline|
29
+ impressions_count.each do |key, value|
30
+ pipeline.hincrby(impressions_count_key, key, value)
31
+ end
32
+
33
+ @future = pipeline.hlen(impressions_count_key)
34
+ end
35
+
36
+ expire_impressions_count_key(impressions_count, result)
37
+ rescue StandardError => e
38
+ @config.log_found_exception(__method__.to_s, e)
39
+ end
40
+
41
+ private
42
+
43
+ def expire_impressions_count_key(impressions_count, pipeline_result)
44
+ total_count = impressions_count.sum { |_, value| value }
45
+ hlen = pipeline_result.last
46
+
47
+ @adapter.expire(impressions_count_key, EXPIRE_SECONDS) if impressions_count.size == hlen && (pipeline_result.sum - hlen) == total_count
48
+ end
49
+
50
+ def impressions_count_key
51
+ "#{@config.redis_namespace}.impressions.count"
52
+ end
53
+
54
+ def unique_keys_key
55
+ "#{@config.redis_namespace}.uniquekeys"
56
+ end
57
+
58
+ def uniques_formatter(uniques)
59
+ return if uniques.nil? || uniques.empty?
60
+
61
+ to_return = []
62
+ uniques.each do |key, value|
63
+ to_return << {
64
+ f: key,
65
+ k: value.to_a
66
+ }
67
+ end
68
+
69
+ to_return
70
+ rescue StandardError => error
71
+ @config.log_found_exception(__method__.to_s, error)
72
+ nil
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -4,12 +4,10 @@ module SplitIoClient
4
4
  module Cache
5
5
  module Senders
6
6
  class ImpressionsCountSender
7
- COUNTER_REFRESH_RATE_SECONDS = 1800
8
-
9
- def initialize(config, impression_counter, impressions_api)
7
+ def initialize(config, impression_counter, impressions_sender_adapter)
10
8
  @config = config
11
9
  @impression_counter = impression_counter
12
- @impressions_api = impressions_api
10
+ @impressions_sender_adapter = impressions_sender_adapter
13
11
  end
14
12
 
15
13
  def call
@@ -22,13 +20,11 @@ module SplitIoClient
22
20
  @config.threads[:impressions_count_sender] = Thread.new do
23
21
  begin
24
22
  @config.logger.info('Starting impressions count service')
25
-
26
23
  loop do
27
- post_impressions_count
28
-
29
- sleep(COUNTER_REFRESH_RATE_SECONDS)
24
+ sleep(@config.counter_refresh_rate)
25
+ post_impressions_count
30
26
  end
31
- rescue SplitIoClient::SDKShutdownException
27
+ rescue SplitIoClient::SDKShutdownException
32
28
  post_impressions_count
33
29
 
34
30
  @config.logger.info('Posting impressions count due to shutdown')
@@ -37,27 +33,7 @@ module SplitIoClient
37
33
  end
38
34
 
39
35
  def post_impressions_count
40
- @impressions_api.post_count(formatter(@impression_counter.pop_all))
41
- rescue StandardError => error
42
- @config.log_found_exception(__method__.to_s, error)
43
- end
44
-
45
- def formatter(counts)
46
- return if counts.empty?
47
-
48
- formated_counts = {pf: []}
49
-
50
- counts.each do |key, value|
51
- key_splited = key.split('::')
52
-
53
- formated_counts[:pf] << {
54
- f: key_splited[0].to_s, # feature name
55
- m: key_splited[1].to_i, # time frame
56
- rc: value # count
57
- }
58
- end
59
-
60
- formated_counts
36
+ @impressions_sender_adapter.record_impressions_count(@impression_counter.pop_all)
61
37
  rescue StandardError => error
62
38
  @config.log_found_exception(__method__.to_s, error)
63
39
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Cache
5
+ module Senders
6
+ class ImpressionsSenderAdapter
7
+ extend Forwardable
8
+ def_delegators :@sender, :record_uniques_key, :record_impressions_count
9
+
10
+ def initialize(config, telemetry_api, impressions_api)
11
+ @sender = case config.telemetry_adapter.class.to_s
12
+ when 'SplitIoClient::Cache::Adapters::RedisAdapter'
13
+ Cache::Senders::RedisImpressionsSender.new(config)
14
+ else
15
+ Cache::Senders::MemoryImpressionsSender.new(config, telemetry_api, impressions_api)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -17,6 +17,14 @@ module SplitIoClient
17
17
  post_telemetry("#{@config.telemetry_service_url}/metrics/usage", stats, 'stats')
18
18
  end
19
19
 
20
+ def record_unique_keys(uniques)
21
+ return if uniques[:mtks].empty?
22
+
23
+ post_telemetry("#{@config.telemetry_service_url}/mtks/ss", uniques, 'mtks')
24
+ rescue StandardError => e
25
+ @config.log_found_exception(__method__.to_s, e)
26
+ end
27
+
20
28
  private
21
29
 
22
30
  def post_telemetry(url, obj, method)
@@ -4,65 +4,75 @@ module SplitIoClient
4
4
  module Engine
5
5
  module Common
6
6
  class ImpressionManager
7
- def initialize(config, impressions_repository, impression_counter, telemetry_runtime_producer)
7
+ def initialize(config,
8
+ impressions_repository,
9
+ impression_counter,
10
+ telemetry_runtime_producer,
11
+ impression_observer,
12
+ unique_keys_tracker,
13
+ impression_router)
8
14
  @config = config
9
15
  @impressions_repository = impressions_repository
10
16
  @impression_counter = impression_counter
11
- @impression_observer = SplitIoClient::Observers::ImpressionObserver.new
17
+ @impression_observer = impression_observer
12
18
  @telemetry_runtime_producer = telemetry_runtime_producer
19
+ @unique_keys_tracker = unique_keys_tracker
20
+ @impression_router = impression_router
13
21
  end
14
22
 
15
- # added param time for test
16
23
  def build_impression(matching_key, bucketing_key, split_name, treatment, params = {})
17
24
  impression_data = impression_data(matching_key, bucketing_key, split_name, treatment, params[:time])
18
25
 
19
- impression_data[:pt] = @impression_observer.test_and_set(impression_data) unless redis?
20
-
21
- @impression_counter.inc(split_name, impression_data[:m]) if optimized? && !redis?
26
+ begin
27
+ case @config.impressions_mode
28
+ when :debug # In DEBUG mode we should calculate the pt only.
29
+ impression_data[:pt] = @impression_observer.test_and_set(impression_data)
30
+ when :none # In NONE mode we should track the total amount of evaluations and the unique keys.
31
+ @impression_counter.inc(split_name, impression_data[:m])
32
+ @unique_keys_tracker.track(split_name, matching_key)
33
+ else # In OPTIMIZED mode we should track the total amount of evaluations and deduplicate the impressions.
34
+ impression_data[:pt] = @impression_observer.test_and_set(impression_data)
35
+ @impression_counter.inc(split_name, impression_data[:m])
36
+ end
37
+ rescue StandardError => e
38
+ @config.log_found_exception(__method__.to_s, e)
39
+ end
22
40
 
23
41
  impression(impression_data, params[:attributes])
24
- rescue StandardError => e
25
- @config.log_found_exception(__method__.to_s, e)
26
42
  end
27
43
 
28
44
  def track(impressions)
29
45
  return if impressions.empty?
30
46
 
31
- impression_router.add_bulk(impressions)
32
-
33
- dropped = 0
34
- queued = 0
35
- dedupe = 0
36
-
37
- if optimized? && !redis?
38
- optimized_impressions = impressions.select { |imp| should_queue_impression?(imp[:i]) }
39
-
40
- unless optimized_impressions.empty?
41
- dropped = @impressions_repository.add_bulk(optimized_impressions)
42
- dedupe = impressions.length - optimized_impressions.length
43
- queued = optimized_impressions.length - dropped
47
+ stats = { dropped: 0, queued: 0, dedupe: 0 }
48
+ begin
49
+ case @config.impressions_mode
50
+ when :none
51
+ return
52
+ when :debug
53
+ track_debug_mode(impressions, stats)
54
+ when :optimized
55
+ track_optimized_mode(impressions, stats)
44
56
  end
45
- else
46
- dropped = @impressions_repository.add_bulk(impressions)
47
- queued = impressions.length - dropped
57
+ rescue StandardError => e
58
+ @config.log_found_exception(__method__.to_s, e)
59
+ ensure
60
+ record_stats(stats)
61
+ @impression_router.add_bulk(impressions)
48
62
  end
49
-
50
- record_stats(queued, dropped, dedupe)
51
- rescue StandardError => e
52
- @config.log_found_exception(__method__.to_s, e)
53
63
  end
54
64
 
55
65
  private
56
66
 
57
- def record_stats(queued, dropped, dedupe)
67
+ def record_stats(stats)
58
68
  return if redis?
59
69
 
60
70
  imp_queued = Telemetry::Domain::Constants::IMPRESSIONS_QUEUED
61
71
  imp_dropped = Telemetry::Domain::Constants::IMPRESSIONS_DROPPED
62
72
  imp_dedupe = Telemetry::Domain::Constants::IMPRESSIONS_DEDUPE
63
- @telemetry_runtime_producer.record_impressions_stats(imp_queued, queued) unless queued.zero?
64
- @telemetry_runtime_producer.record_impressions_stats(imp_dropped, dropped) unless dropped.zero?
65
- @telemetry_runtime_producer.record_impressions_stats(imp_dedupe, dedupe) unless dedupe.zero?
73
+ @telemetry_runtime_producer.record_impressions_stats(imp_queued, stats[:queued]) unless stats[:queued].zero?
74
+ @telemetry_runtime_producer.record_impressions_stats(imp_dropped, stats[:dropped]) unless stats[:dropped].zero?
75
+ @telemetry_runtime_producer.record_impressions_stats(imp_dedupe, stats[:dedupe]) unless stats[:dedupe].zero?
66
76
  end
67
77
 
68
78
  # added param time for test
@@ -91,10 +101,6 @@ module SplitIoClient
91
101
  @config.labels_enabled ? label : nil
92
102
  end
93
103
 
94
- def optimized?
95
- @config.impressions_mode == :optimized
96
- end
97
-
98
104
  def should_queue_impression?(impression)
99
105
  impression[:pt].nil? ||
100
106
  (ImpressionCounter.truncate_time_frame(impression[:pt]) != ImpressionCounter.truncate_time_frame(impression[:m]))
@@ -108,10 +114,19 @@ module SplitIoClient
108
114
  @config.impressions_adapter.class.to_s == 'SplitIoClient::Cache::Adapters::RedisAdapter'
109
115
  end
110
116
 
111
- def impression_router
112
- @impression_router ||= SplitIoClient::ImpressionRouter.new(@config)
113
- rescue StandardError => error
114
- @config.log_found_exception(__method__.to_s, error)
117
+ def track_debug_mode(impressions, stats)
118
+ stats[:dropped] = @impressions_repository.add_bulk(impressions)
119
+ stats[:queued] = impressions.length - stats[:dropped]
120
+ end
121
+
122
+ def track_optimized_mode(impressions, stats)
123
+ optimized_impressions = impressions.select { |imp| should_queue_impression?(imp[:i]) }
124
+
125
+ return if optimized_impressions.empty?
126
+
127
+ stats[:dropped] = @impressions_repository.add_bulk(optimized_impressions)
128
+ stats[:dedupe] = impressions.length - optimized_impressions.length
129
+ stats[:queued] = optimized_impressions.length - stats[:dropped]
115
130
  end
116
131
  end
117
132
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+
5
+ module SplitIoClient
6
+ module Engine
7
+ module Common
8
+ class NoopImpressionCounter
9
+ def inc(split_name, time_frame)
10
+ # no-op
11
+ end
12
+
13
+ def pop_all
14
+ # no-op
15
+ end
16
+
17
+ def make_key(split_name, time_frame)
18
+ # no-op
19
+ end
20
+
21
+ def self.truncate_time_frame(timestamp_ms)
22
+ # no-op
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Engine
5
+ module Impressions
6
+ class NoopUniqueKeysTracker
7
+ def call
8
+ # no-op
9
+ end
10
+
11
+ def track(feature_name, key)
12
+ # no-op
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Engine
5
+ module Impressions
6
+ class UniqueKeysTracker
7
+ INTERVAL_TO_CLEAR_LONG_TERM_CACHE = 86_400 # 24 hours
8
+
9
+ def initialize(config,
10
+ filter_adapter,
11
+ sender_adapter,
12
+ cache)
13
+ @config = config
14
+ @filter_adapter = filter_adapter
15
+ @sender_adapter = sender_adapter
16
+ @cache = cache
17
+ @cache_max_size = config.unique_keys_cache_max_size
18
+ @max_bulk_size = config.unique_keys_bulk_size
19
+ @semaphore = Mutex.new
20
+ end
21
+
22
+ def call
23
+ @config.threads[:unique_keys_sender] = Thread.new { send_bulk_data_thread }
24
+ @config.threads[:clear_filter] = Thread.new { clear_filter_thread }
25
+ end
26
+
27
+ def track(feature_name, key)
28
+ return false if @filter_adapter.contains?(feature_name, key)
29
+
30
+ @filter_adapter.add(feature_name, key)
31
+
32
+ add_or_update(feature_name, key)
33
+
34
+ send_bulk_data if @cache.size >= @cache_max_size
35
+
36
+ true
37
+ rescue StandardError => e
38
+ @config.log_found_exception(__method__.to_s, e)
39
+ false
40
+ end
41
+
42
+ private
43
+
44
+ def send_bulk_data_thread
45
+ @config.logger.info('Starting Unique Keys Tracker.') if @config.debug_enabled
46
+ loop do
47
+ sleep(@config.unique_keys_refresh_rate)
48
+ send_bulk_data
49
+ end
50
+ rescue SplitIoClient::SDKShutdownException
51
+ send_bulk_data
52
+ @config.logger.info('Posting unique keys due to shutdown')
53
+ end
54
+
55
+ def clear_filter_thread
56
+ loop do
57
+ sleep(INTERVAL_TO_CLEAR_LONG_TERM_CACHE)
58
+ @config.logger.debug('Starting task to clean the filter cache.') if @config.debug_enabled
59
+ @filter_adapter.clear
60
+ end
61
+ rescue SplitIoClient::SDKShutdownException
62
+ @filter_adapter.clear
63
+ end
64
+
65
+ def add_or_update(feature_name, key)
66
+ if @cache[feature_name].nil?
67
+ @cache[feature_name] = Set.new([key])
68
+ else
69
+ @cache[feature_name].add(key)
70
+ end
71
+ end
72
+
73
+ def send_bulk_data
74
+ @semaphore.synchronize do
75
+ return if @cache.empty?
76
+
77
+ uniques = @cache.clone
78
+ @cache.clear
79
+
80
+ if uniques.size <= @max_bulk_size
81
+ @sender_adapter.record_uniques_key(uniques)
82
+ return
83
+ end
84
+
85
+ bulks = SplitIoClient::Utilities.split_bulk_to_send(uniques, uniques.size / @max_bulk_size)
86
+
87
+ bulks.each do |b|
88
+ @sender_adapter.record_uniques_key(b)
89
+ end
90
+ end
91
+ rescue StandardError => e
92
+ @config.log_found_exception(__method__.to_s, e)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -52,9 +52,8 @@ module SplitIoClient
52
52
  # @return [boolean] match value for combiner delegates
53
53
  def eval_and(args)
54
54
  # Convert all keys to symbols
55
- if args && args[:attributes]
56
- args[:attributes] = args[:attributes].each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v }
57
- end
55
+ args[:attributes] = args[:attributes].each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v } if args && args[:attributes]
56
+
58
57
  @matchers.all? do |matcher|
59
58
  if match_with_key?(matcher)
60
59
  matcher.match?(value: args[:matching_key])
@@ -30,6 +30,11 @@ module SplitIoClient
30
30
  PhusionPassenger.on_event(:starting_worker_process) { |forked| start_thread if forked } if defined?(PhusionPassenger)
31
31
  end
32
32
 
33
+ def start_consumer
34
+ start_consumer_thread
35
+ PhusionPassenger.on_event(:starting_worker_process) { |forked| start_consumer_thread if forked } if defined?(PhusionPassenger)
36
+ end
37
+
33
38
  private
34
39
 
35
40
  def start_thread
@@ -55,6 +60,14 @@ module SplitIoClient
55
60
  end
56
61
  end
57
62
 
63
+ def start_consumer_thread
64
+ @config.threads[:start_sdk_consumer] = Thread.new do
65
+ @status_manager.ready!
66
+ @telemetry_synchronizer.synchronize_config
67
+ @synchronizer.start_periodic_data_recording
68
+ end
69
+ end
70
+
58
71
  def process_subsystem_ready
59
72
  @synchronizer.stop_periodic_fetch
60
73
  @synchronizer.sync_all
@@ -12,7 +12,6 @@ module SplitIoClient
12
12
 
13
13
  def initialize(
14
14
  repositories,
15
- api_key,
16
15
  config,
17
16
  params
18
17
  )
@@ -20,13 +19,14 @@ module SplitIoClient
20
19
  @segments_repository = repositories[:segments]
21
20
  @impressions_repository = repositories[:impressions]
22
21
  @events_repository = repositories[:events]
23
- @api_key = api_key
24
22
  @config = config
25
23
  @split_fetcher = params[:split_fetcher]
26
24
  @segment_fetcher = params[:segment_fetcher]
27
- @impressions_api = SplitIoClient::Api::Impressions.new(@api_key, @config, params[:telemetry_runtime_producer])
25
+ @impressions_api = params[:impressions_api]
28
26
  @impression_counter = params[:imp_counter]
29
27
  @telemetry_synchronizer = params[:telemetry_synchronizer]
28
+ @impressions_sender_adapter = params[:impressions_sender_adapter]
29
+ @unique_keys_tracker = params[:unique_keys_tracker]
30
30
  end
31
31
 
32
32
  def sync_all(asynchronous = true)
@@ -42,10 +42,14 @@ module SplitIoClient
42
42
  end
43
43
 
44
44
  def start_periodic_data_recording
45
- impressions_sender
46
- events_sender
45
+ unless @config.consumer?
46
+ impressions_sender
47
+ events_sender
48
+ start_telemetry_sync_task
49
+ end
50
+
47
51
  impressions_count_sender
48
- start_telemetry_sync_task
52
+ start_unique_keys_tracker_task
49
53
  end
50
54
 
51
55
  def start_periodic_fetch
@@ -170,7 +174,7 @@ module SplitIoClient
170
174
 
171
175
  # Starts thread which loops constantly and sends impressions to the Split API
172
176
  def impressions_sender
173
- ImpressionsSender.new(@impressions_repository, @config, @impressions_api).call
177
+ ImpressionsSender.new(@impressions_repository, @config, @impressions_api).call unless @config.impressions_mode == :none
174
178
  end
175
179
 
176
180
  # Starts thread which loops constantly and sends events to the Split API
@@ -180,13 +184,17 @@ module SplitIoClient
180
184
 
181
185
  # Starts thread which loops constantly and sends impressions count to the Split API
182
186
  def impressions_count_sender
183
- ImpressionsCountSender.new(@config, @impression_counter, @impressions_api).call
187
+ ImpressionsCountSender.new(@config, @impression_counter, @impressions_sender_adapter).call unless @config.impressions_mode == :debug
184
188
  end
185
189
 
186
190
  def start_telemetry_sync_task
187
191
  Telemetry::SyncTask.new(@config, @telemetry_synchronizer).call
188
192
  end
189
193
 
194
+ def start_unique_keys_tracker_task
195
+ @unique_keys_tracker.call
196
+ end
197
+
190
198
  def sync_result(success, remaining_attempts, segment_names = nil)
191
199
  { success: success, remaining_attempts: remaining_attempts, segment_names: segment_names }
192
200
  end
@@ -111,6 +111,12 @@ module SplitIoClient
111
111
  @telemetry_refresh_rate = SplitConfig.init_telemetry_refresh_rate(opts[:telemetry_refresh_rate])
112
112
  @telemetry_service_url = opts[:telemetry_service_url] || SplitConfig.default_telemetry_service_url
113
113
 
114
+ @unique_keys_refresh_rate = SplitConfig.default_unique_keys_refresh_rate(@cache_adapter)
115
+ @unique_keys_cache_max_size = SplitConfig.default_unique_keys_cache_max_size
116
+ @unique_keys_bulk_size = SplitConfig.default_unique_keys_bulk_size(@cache_adapter)
117
+
118
+ @counter_refresh_rate = SplitConfig.default_counter_refresh_rate(@cache_adapter)
119
+
114
120
  @sdk_start_time = Time.now
115
121
 
116
122
  @on_demand_fetch_retry_delay_seconds = SplitConfig.default_on_demand_fetch_retry_delay_seconds
@@ -284,6 +290,18 @@ module SplitIoClient
284
290
  attr_accessor :on_demand_fetch_retry_delay_seconds
285
291
  attr_accessor :on_demand_fetch_max_retries
286
292
 
293
+ attr_accessor :unique_keys_refresh_rate
294
+ attr_accessor :unique_keys_cache_max_size
295
+ attr_accessor :unique_keys_bulk_size
296
+
297
+ attr_accessor :counter_refresh_rate
298
+
299
+ def self.default_counter_refresh_rate(adapter)
300
+ return 300 if adapter == :redis # Send bulk impressions count - Refresh rate: 5 min.
301
+
302
+ 1800 # Send bulk impressions count - Refresh rate: 30 min.
303
+ end
304
+
287
305
  def self.default_on_demand_fetch_retry_delay_seconds
288
306
  0.05
289
307
  end
@@ -302,6 +320,8 @@ module SplitIoClient
302
320
  case impressions_mode
303
321
  when :debug
304
322
  return :debug
323
+ when :none
324
+ return :none
305
325
  else
306
326
  @logger.error('You passed an invalid impressions_mode, impressions_mode should be one of the following values: :debug or :optimized. Defaulting to :optimized mode') unless impressions_mode == :optimized
307
327
  return :optimized
@@ -468,6 +488,22 @@ module SplitIoClient
468
488
  3600
469
489
  end
470
490
 
491
+ def self.default_unique_keys_refresh_rate(adapter)
492
+ return 300 if adapter == :redis
493
+
494
+ 900
495
+ end
496
+
497
+ def self.default_unique_keys_cache_max_size
498
+ 30000
499
+ end
500
+
501
+ def self.default_unique_keys_bulk_size(adapter)
502
+ return 2000 if adapter == :redis
503
+
504
+ 5000
505
+ end
506
+
471
507
  def self.default_telemetry_service_url
472
508
  'https://telemetry.split.io/api/v1'
473
509
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SplitIoClient
2
4
  class SplitFactory
3
5
  ROOT_PROCESS_ID = Process.pid
@@ -34,8 +36,10 @@ module SplitIoClient
34
36
 
35
37
  build_telemetry_components
36
38
  build_repositories
37
- build_impressions_components
38
39
  build_telemetry_synchronizer
40
+ build_impressions_sender_adapter
41
+ build_unique_keys_tracker
42
+ build_impressions_components
39
43
 
40
44
  @status_manager = Engine::StatusManager.new(@config)
41
45
 
@@ -49,8 +53,10 @@ module SplitIoClient
49
53
  return start_localhost_components if @config.localhost_mode
50
54
 
51
55
  if @config.consumer?
52
- @status_manager.ready!
53
- @telemetry_synchronizer.synchronize_config
56
+ build_synchronizer
57
+ build_sync_manager
58
+
59
+ @sync_manager.start_consumer
54
60
  return
55
61
  end
56
62
 
@@ -59,7 +65,7 @@ module SplitIoClient
59
65
  build_streaming_components
60
66
  build_sync_manager
61
67
 
62
- @sync_manager.start
68
+ @sync_manager.start if @config.valid_mode
63
69
  end
64
70
 
65
71
  def stop!
@@ -121,10 +127,10 @@ module SplitIoClient
121
127
  def validate_api_key
122
128
  if(@api_key.nil?)
123
129
  @config.logger.error('Factory Instantiation: you passed a nil api_key, api_key must be a non-empty String')
124
- @config.valid_mode = false
130
+ @config.valid_mode = false
125
131
  elsif (@api_key.empty?)
126
132
  @config.logger.error('Factory Instantiation: you passed and empty api_key, api_key must be a non-empty String')
127
- @config.valid_mode = false
133
+ @config.valid_mode = false
128
134
  end
129
135
  end
130
136
 
@@ -167,11 +173,13 @@ module SplitIoClient
167
173
  split_fetcher: @split_fetcher,
168
174
  segment_fetcher: @segment_fetcher,
169
175
  imp_counter: @impression_counter,
170
- telemetry_runtime_producer: @runtime_producer,
171
- telemetry_synchronizer: @telemetry_synchronizer
176
+ telemetry_synchronizer: @telemetry_synchronizer,
177
+ impressions_sender_adapter: @impressions_sender_adapter,
178
+ impressions_api: @impressions_api,
179
+ unique_keys_tracker: @unique_keys_tracker
172
180
  }
173
181
 
174
- @synchronizer = Engine::Synchronizer.new(repositories, @api_key, @config, params)
182
+ @synchronizer = Engine::Synchronizer.new(repositories, @config, params)
175
183
  end
176
184
 
177
185
  def build_streaming_components
@@ -198,13 +206,51 @@ module SplitIoClient
198
206
  end
199
207
 
200
208
  def build_telemetry_synchronizer
201
- telemetry_api = Api::TelemetryApi.new(@config, @api_key, @runtime_producer)
202
- @telemetry_synchronizer = Telemetry::Synchronizer.new(@config, @telemetry_consumers, @init_producer, repositories, telemetry_api)
209
+ @telemetry_api = Api::TelemetryApi.new(@config, @api_key, @runtime_producer)
210
+ @telemetry_synchronizer = Telemetry::Synchronizer.new(@config, @telemetry_consumers, @init_producer, repositories, @telemetry_api)
211
+ end
212
+
213
+ def build_unique_keys_tracker
214
+ if @config.impressions_mode != :none
215
+ @unique_keys_tracker = Engine::Impressions::NoopUniqueKeysTracker.new
216
+ return
217
+ end
218
+
219
+ bf = Cache::Filter::BloomFilter.new(30_000_000)
220
+ filter_adapter = Cache::Filter::FilterAdapter.new(@config, bf)
221
+ cache = Concurrent::Hash.new
222
+ @unique_keys_tracker = Engine::Impressions::UniqueKeysTracker.new(@config, filter_adapter, @impressions_sender_adapter, cache)
223
+ end
224
+
225
+ def build_impressions_observer
226
+ if (@config.cache_adapter == :redis && @config.impressions_mode != :optimized) ||
227
+ (@config.cache_adapter == :memory && @config.impressions_mode == :none)
228
+ @impression_observer = Observers::NoopImpressionObserver.new
229
+ else
230
+ @impression_observer = Observers::ImpressionObserver.new
231
+ end
232
+ end
233
+
234
+ def build_impression_counter
235
+ case @config.impressions_mode
236
+ when :debug
237
+ @impression_counter = Engine::Common::NoopImpressionCounter.new
238
+ else
239
+ @impression_counter = Engine::Common::ImpressionCounter.new
240
+ end
241
+ end
242
+
243
+ def build_impressions_sender_adapter
244
+ @impressions_api = Api::Impressions.new(@api_key, @config, @runtime_producer)
245
+ @impressions_sender_adapter = Cache::Senders::ImpressionsSenderAdapter.new(@config, @telemetry_api, @impressions_api)
203
246
  end
204
247
 
205
248
  def build_impressions_components
206
- @impression_counter = Engine::Common::ImpressionCounter.new
207
- @impressions_manager = Engine::Common::ImpressionManager.new(@config, @impressions_repository, @impression_counter, @runtime_producer)
249
+ build_impressions_observer
250
+ build_impression_counter
251
+
252
+ impression_router = ImpressionRouter.new(@config)
253
+ @impressions_manager = Engine::Common::ImpressionManager.new(@config, @impressions_repository, @impression_counter, @runtime_producer, @impression_observer, @unique_keys_tracker, impression_router)
208
254
  end
209
255
  end
210
256
  end
@@ -183,8 +183,10 @@ module SplitIoClient
183
183
  case @config.impressions_mode
184
184
  when :optimized
185
185
  0
186
- else
186
+ when :debug
187
187
  1
188
+ else
189
+ 2
188
190
  end
189
191
  end
190
192
  end
@@ -37,5 +37,17 @@ module SplitIoClient
37
37
 
38
38
  interval * random_factor
39
39
  end
40
+
41
+ def split_bulk_to_send(hash, divisions)
42
+ count = 0
43
+
44
+ hash.each_with_object([]) do |key_value, final|
45
+ final[count % divisions] ||= {}
46
+ final[count % divisions][key_value[0]] = key_value[1]
47
+ count += 1
48
+ end
49
+ rescue StandardError
50
+ []
51
+ end
40
52
  end
41
53
  end
@@ -1,3 +1,3 @@
1
1
  module SplitIoClient
2
- VERSION = '7.3.4'
2
+ VERSION = '7.3.5.pre.rc3'
3
3
  end
@@ -12,8 +12,11 @@ require 'splitclient-rb/cache/adapters/memory_adapter'
12
12
  require 'splitclient-rb/cache/adapters/redis_adapter'
13
13
  require 'splitclient-rb/cache/fetchers/segment_fetcher'
14
14
  require 'splitclient-rb/cache/fetchers/split_fetcher'
15
+ require 'splitclient-rb/cache/filter/bloom_filter'
16
+ require 'splitclient-rb/cache/filter/filter_adapter'
15
17
  require 'splitclient-rb/cache/hashers/impression_hasher'
16
18
  require 'splitclient-rb/cache/observers/impression_observer'
19
+ require 'splitclient-rb/cache/observers/noop_impression_observer'
17
20
  require 'splitclient-rb/cache/repositories/repository'
18
21
  require 'splitclient-rb/cache/repositories/segments_repository'
19
22
  require 'splitclient-rb/cache/repositories/splits_repository'
@@ -28,6 +31,9 @@ require 'splitclient-rb/cache/senders/impressions_sender'
28
31
  require 'splitclient-rb/cache/senders/events_sender'
29
32
  require 'splitclient-rb/cache/senders/impressions_count_sender'
30
33
  require 'splitclient-rb/cache/senders/localhost_repo_cleaner'
34
+ require 'splitclient-rb/cache/senders/impressions_sender_adapter'
35
+ require 'splitclient-rb/cache/senders/impressions_adapter/memory_sender'
36
+ require 'splitclient-rb/cache/senders/impressions_adapter/redis_sender'
31
37
  require 'splitclient-rb/cache/stores/localhost_split_builder'
32
38
  require 'splitclient-rb/cache/stores/localhost_split_store'
33
39
  require 'splitclient-rb/cache/stores/store_utils'
@@ -52,6 +58,7 @@ require 'splitclient-rb/engine/api/events'
52
58
  require 'splitclient-rb/engine/api/telemetry_api'
53
59
  require 'splitclient-rb/engine/common/impressions_counter'
54
60
  require 'splitclient-rb/engine/common/impressions_manager'
61
+ require 'splitclient-rb/engine/common/noop_impressions_counter'
55
62
  require 'splitclient-rb/engine/parser/condition'
56
63
  require 'splitclient-rb/engine/parser/partition'
57
64
  require 'splitclient-rb/engine/parser/evaluator'
@@ -79,6 +86,8 @@ require 'splitclient-rb/engine/matchers/equal_to_boolean_matcher'
79
86
  require 'splitclient-rb/engine/matchers/equal_to_matcher'
80
87
  require 'splitclient-rb/engine/matchers/matches_string_matcher'
81
88
  require 'splitclient-rb/engine/evaluator/splitter'
89
+ require 'splitclient-rb/engine/impressions/noop_unique_keys_tracker'
90
+ require 'splitclient-rb/engine/impressions/unique_keys_tracker'
82
91
  require 'splitclient-rb/engine/metrics/binary_search_latency_tracker'
83
92
  require 'splitclient-rb/engine/models/split'
84
93
  require 'splitclient-rb/engine/models/label'
@@ -50,6 +50,7 @@ Gem::Specification.new do |spec|
50
50
  spec.add_development_dependency 'timecop', '~> 0.9'
51
51
  spec.add_development_dependency 'webmock', '~> 3.14'
52
52
 
53
+ spec.add_runtime_dependency 'bitarray', '~> 1.3'
53
54
  spec.add_runtime_dependency 'concurrent-ruby', '~> 1.0'
54
55
  spec.add_runtime_dependency 'faraday', '>= 0.8', '< 2.0'
55
56
  spec.add_runtime_dependency 'json', '>= 1.8', '< 3.0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: splitclient-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.3.4
4
+ version: 7.3.5.pre.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Split Software
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-22 00:00:00.000000000 Z
11
+ date: 2022-04-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: allocation_stats
@@ -192,6 +192,20 @@ dependencies:
192
192
  - - "~>"
193
193
  - !ruby/object:Gem::Version
194
194
  version: '3.14'
195
+ - !ruby/object:Gem::Dependency
196
+ name: bitarray
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '1.3'
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: '1.3'
195
209
  - !ruby/object:Gem::Dependency
196
210
  name: concurrent-ruby
197
211
  requirement: !ruby/object:Gem::Requirement
@@ -393,8 +407,11 @@ files:
393
407
  - lib/splitclient-rb/cache/adapters/redis_adapter.rb
394
408
  - lib/splitclient-rb/cache/fetchers/segment_fetcher.rb
395
409
  - lib/splitclient-rb/cache/fetchers/split_fetcher.rb
410
+ - lib/splitclient-rb/cache/filter/bloom_filter.rb
411
+ - lib/splitclient-rb/cache/filter/filter_adapter.rb
396
412
  - lib/splitclient-rb/cache/hashers/impression_hasher.rb
397
413
  - lib/splitclient-rb/cache/observers/impression_observer.rb
414
+ - lib/splitclient-rb/cache/observers/noop_impression_observer.rb
398
415
  - lib/splitclient-rb/cache/repositories/events/memory_repository.rb
399
416
  - lib/splitclient-rb/cache/repositories/events/redis_repository.rb
400
417
  - lib/splitclient-rb/cache/repositories/events_repository.rb
@@ -406,9 +423,12 @@ files:
406
423
  - lib/splitclient-rb/cache/repositories/splits_repository.rb
407
424
  - lib/splitclient-rb/cache/routers/impression_router.rb
408
425
  - lib/splitclient-rb/cache/senders/events_sender.rb
426
+ - lib/splitclient-rb/cache/senders/impressions_adapter/memory_sender.rb
427
+ - lib/splitclient-rb/cache/senders/impressions_adapter/redis_sender.rb
409
428
  - lib/splitclient-rb/cache/senders/impressions_count_sender.rb
410
429
  - lib/splitclient-rb/cache/senders/impressions_formatter.rb
411
430
  - lib/splitclient-rb/cache/senders/impressions_sender.rb
431
+ - lib/splitclient-rb/cache/senders/impressions_sender_adapter.rb
412
432
  - lib/splitclient-rb/cache/senders/localhost_repo_cleaner.rb
413
433
  - lib/splitclient-rb/cache/stores/localhost_split_builder.rb
414
434
  - lib/splitclient-rb/cache/stores/localhost_split_store.rb
@@ -427,7 +447,10 @@ files:
427
447
  - lib/splitclient-rb/engine/back_off.rb
428
448
  - lib/splitclient-rb/engine/common/impressions_counter.rb
429
449
  - lib/splitclient-rb/engine/common/impressions_manager.rb
450
+ - lib/splitclient-rb/engine/common/noop_impressions_counter.rb
430
451
  - lib/splitclient-rb/engine/evaluator/splitter.rb
452
+ - lib/splitclient-rb/engine/impressions/noop_unique_keys_tracker.rb
453
+ - lib/splitclient-rb/engine/impressions/unique_keys_tracker.rb
431
454
  - lib/splitclient-rb/engine/matchers/all_keys_matcher.rb
432
455
  - lib/splitclient-rb/engine/matchers/between_matcher.rb
433
456
  - lib/splitclient-rb/engine/matchers/combiners.rb
@@ -520,9 +543,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
520
543
  version: '0'
521
544
  required_rubygems_version: !ruby/object:Gem::Requirement
522
545
  requirements:
523
- - - ">="
546
+ - - ">"
524
547
  - !ruby/object:Gem::Version
525
- version: '0'
548
+ version: 1.3.1
526
549
  requirements: []
527
550
  rubygems_version: 3.2.32
528
551
  signing_key: