splitclient-rb 8.4.0 → 8.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -2
  3. data/CHANGES.txt +24 -0
  4. data/LICENSE +169 -13
  5. data/NOTICE.txt +5 -0
  6. data/lib/splitclient-rb/cache/fetchers/split_fetcher.rb +9 -7
  7. data/lib/splitclient-rb/cache/filter/bloom_filter.rb +10 -6
  8. data/lib/splitclient-rb/cache/repositories/impressions/redis_repository.rb +1 -0
  9. data/lib/splitclient-rb/cache/repositories/rule_based_segments_repository.rb +136 -0
  10. data/lib/splitclient-rb/cache/repositories/segments_repository.rb +21 -2
  11. data/lib/splitclient-rb/cache/repositories/splits_repository.rb +42 -12
  12. data/lib/splitclient-rb/cache/senders/impressions_formatter.rb +24 -10
  13. data/lib/splitclient-rb/cache/stores/localhost_split_builder.rb +2 -1
  14. data/lib/splitclient-rb/clients/split_client.rb +146 -62
  15. data/lib/splitclient-rb/engine/api/client.rb +8 -0
  16. data/lib/splitclient-rb/engine/api/faraday_middleware/gzip.rb +1 -0
  17. data/lib/splitclient-rb/engine/api/splits.rb +100 -24
  18. data/lib/splitclient-rb/engine/auth_api_client.rb +3 -3
  19. data/lib/splitclient-rb/engine/common/impressions_manager.rb +56 -30
  20. data/lib/splitclient-rb/engine/events/events_delivery.rb +20 -0
  21. data/lib/splitclient-rb/engine/events/events_manager.rb +194 -0
  22. data/lib/splitclient-rb/engine/events/events_manager_config.rb +96 -0
  23. data/lib/splitclient-rb/engine/events/events_task.rb +50 -0
  24. data/lib/splitclient-rb/engine/events/noop_events_queue.rb +13 -0
  25. data/lib/splitclient-rb/engine/fallback_treatment_calculator.rb +48 -0
  26. data/lib/splitclient-rb/engine/impressions/unique_keys_tracker.rb +58 -11
  27. data/lib/splitclient-rb/engine/matchers/combining_matcher.rb +3 -1
  28. data/lib/splitclient-rb/engine/matchers/prerequisites_matcher.rb +31 -0
  29. data/lib/splitclient-rb/engine/matchers/rule_based_segment_matcher.rb +78 -0
  30. data/lib/splitclient-rb/engine/models/evaluation_options.rb +9 -0
  31. data/lib/splitclient-rb/engine/models/event_active_subscriptions.rb +14 -0
  32. data/lib/splitclient-rb/engine/models/events_metadata.rb +10 -0
  33. data/lib/splitclient-rb/engine/models/fallback_treatment.rb +11 -0
  34. data/lib/splitclient-rb/engine/models/fallback_treatments_configuration.rb +36 -0
  35. data/lib/splitclient-rb/engine/models/label.rb +1 -0
  36. data/lib/splitclient-rb/engine/models/sdk_event.rb +4 -0
  37. data/lib/splitclient-rb/engine/models/sdk_event_type.rb +4 -0
  38. data/lib/splitclient-rb/engine/models/sdk_internal_event.rb +8 -0
  39. data/lib/splitclient-rb/engine/models/sdk_internal_event_notification.rb +14 -0
  40. data/lib/splitclient-rb/engine/models/segment_type.rb +4 -0
  41. data/lib/splitclient-rb/engine/models/split_http_response.rb +19 -0
  42. data/lib/splitclient-rb/engine/models/valid_sdk_event.rb +14 -0
  43. data/lib/splitclient-rb/engine/parser/condition.rb +9 -0
  44. data/lib/splitclient-rb/engine/parser/evaluator.rb +24 -30
  45. data/lib/splitclient-rb/engine/status_manager.rb +7 -1
  46. data/lib/splitclient-rb/engine/sync_manager.rb +6 -6
  47. data/lib/splitclient-rb/engine/synchronizer.rb +37 -17
  48. data/lib/splitclient-rb/helpers/evaluator_helper.rb +37 -0
  49. data/lib/splitclient-rb/helpers/repository_helper.rb +39 -1
  50. data/lib/splitclient-rb/helpers/util.rb +12 -3
  51. data/lib/splitclient-rb/managers/split_manager.rb +2 -1
  52. data/lib/splitclient-rb/spec.rb +1 -1
  53. data/lib/splitclient-rb/split_config.rb +45 -5
  54. data/lib/splitclient-rb/split_factory.rb +27 -21
  55. data/lib/splitclient-rb/sse/event_source/client.rb +86 -29
  56. data/lib/splitclient-rb/sse/event_source/event_parser.rb +2 -2
  57. data/lib/splitclient-rb/sse/event_source/event_types.rb +1 -0
  58. data/lib/splitclient-rb/sse/notification_manager_keeper.rb +3 -3
  59. data/lib/splitclient-rb/sse/notification_processor.rb +3 -1
  60. data/lib/splitclient-rb/sse/workers/segments_worker.rb +4 -4
  61. data/lib/splitclient-rb/sse/workers/splits_worker.rb +63 -24
  62. data/lib/splitclient-rb/utilitites.rb +6 -10
  63. data/lib/splitclient-rb/validators.rb +56 -25
  64. data/lib/splitclient-rb/version.rb +1 -1
  65. data/lib/splitclient-rb.rb +22 -0
  66. data/sonar-project.properties +1 -0
  67. data/splitclient-rb.gemspec +3 -4
  68. metadata +32 -38
@@ -5,58 +5,134 @@ module SplitIoClient
5
5
  # Retrieves split definitions from the Split Backend
6
6
  class Splits < Client
7
7
 
8
+ PROXY_CHECK_INTERVAL_SECONDS = 24 * 60 * 60
9
+ SPEC_1_1 = "1.1"
10
+
8
11
  def initialize(api_key, config, telemetry_runtime_producer)
9
12
  super(config)
10
13
  @api_key = api_key
11
14
  @telemetry_runtime_producer = telemetry_runtime_producer
12
15
  @flag_sets_filter = @config.flag_sets_filter
16
+ @spec_version = SplitIoClient::Spec::FeatureFlags::SPEC_VERSION
17
+ @last_proxy_check_timestamp = 0
18
+ @clear_storage = false
19
+ @old_spec_since = nil
13
20
  end
14
21
 
15
- def since(since, fetch_options = { cache_control_headers: false, till: nil, sets: nil})
22
+ def since(since, since_rbs, fetch_options = { cache_control_headers: false, till: nil, sets: nil})
16
23
  start = Time.now
24
+
25
+ if check_last_proxy_check_timestamp
26
+ @spec_version = SplitIoClient::Spec::FeatureFlags::SPEC_VERSION
27
+ @config.logger.debug("Switching to new Feature flag spec #{@spec_version} and fetching.") if @config.debug_enabled
28
+ @old_spec_since = since
29
+ since = -1
30
+ since_rbs = -1
31
+ fetch_options = { cache_control_headers: false, till: nil, sets: nil}
32
+ end
33
+
34
+ if @spec_version == Splits::SPEC_1_1
35
+ since = @old_spec_since unless @old_spec_since.nil?
36
+ params = { s: @spec_version, since: since }
37
+ @old_spec_since = nil
38
+ else
39
+ params = { s: @spec_version, since: since, rbSince: since_rbs }
40
+ end
17
41
 
18
- params = { s: SplitIoClient::Spec::FeatureFlags::SPEC_VERSION, since: since }
19
42
  params[:sets] = @flag_sets_filter.join(",") unless @flag_sets_filter.empty?
20
43
  params[:till] = fetch_options[:till] unless fetch_options[:till].nil?
21
- @config.logger.debug("Fetching from splitChanges with #{params}: ")
44
+ @config.logger.debug("Fetching from splitChanges with #{params}: ") if @config.debug_enabled
22
45
  response = get_api("#{@config.base_uri}/splitChanges", @api_key, params, fetch_options[:cache_control_headers])
23
46
  if response.status == 414
24
47
  @config.logger.error("Error fetching feature flags; the amount of flag sets provided are too big, causing uri length error.")
25
48
  raise ApiException.new response.body, 414
26
49
  end
50
+
51
+ if response.status == 400 and sdk_url_overriden? and @spec_version == SplitIoClient::Spec::FeatureFlags::SPEC_VERSION
52
+ @config.logger.warn("Detected proxy response error, changing spec version from #{@spec_version} to #{Splits::SPEC_1_1} and re-fetching.")
53
+ @spec_version = Splits::SPEC_1_1
54
+ @last_proxy_check_timestamp = Time.now
55
+ return since(since, 0, fetch_options = {cache_control_headers: fetch_options[:cache_control_headers], till: fetch_options[:till],
56
+ sets: fetch_options[:sets]})
57
+ end
27
58
  if response.success?
28
- result = splits_with_segment_names(response.body)
29
- unless result[:splits].empty?
30
- @config.split_logger.log_if_debug("#{result[:splits].length} feature flags retrieved. since=#{since}")
31
- end
32
- @config.split_logger.log_if_transport("Feature flag changes response: #{result.to_s}")
59
+ result = JSON.parse(response.body, symbolize_names: true)
33
60
 
34
- bucket = BinarySearchLatencyTracker.get_bucket((Time.now - start) * 1000.0)
35
- @telemetry_runtime_producer.record_sync_latency(Telemetry::Domain::Constants::SPLIT_SYNC, bucket)
36
- @telemetry_runtime_producer.record_successful_sync(Telemetry::Domain::Constants::SPLIT_SYNC, (Time.now.to_f * 1000.0).to_i)
61
+ return process_result(result, since, since_rbs, start)
62
+ end
63
+ @telemetry_runtime_producer.record_sync_error(Telemetry::Domain::Constants::SPLIT_SYNC, response.status)
37
64
 
38
- result
39
- else
40
- @telemetry_runtime_producer.record_sync_error(Telemetry::Domain::Constants::SPLIT_SYNC, response.status)
65
+ @config.logger.error("Unexpected status code while fetching feature flags: #{response.status}. " \
66
+ 'Check your API key and base URI')
41
67
 
42
- @config.logger.error("Unexpected status code while fetching feature flags: #{response.status}. " \
43
- 'Check your API key and base URI')
68
+ raise 'Split SDK failed to connect to backend to fetch feature flags definitions'
69
+ end
44
70
 
45
- raise 'Split SDK failed to connect to backend to fetch feature flags definitions'
46
- end
71
+ def clear_storage
72
+ @clear_storage
47
73
  end
48
74
 
49
75
  private
50
76
 
51
- def splits_with_segment_names(splits_json)
52
- parsed_splits = JSON.parse(splits_json, symbolize_names: true)
77
+ def process_result(result, since, since_rbs, start)
78
+ if @spec_version == Splits::SPEC_1_1
79
+ result = convert_to_new_spec(result)
80
+ end
81
+
82
+ result[:rbs][:d] = check_rbs_data(result[:rbs][:d])
83
+ result = objects_with_segment_names(result)
84
+
85
+ if @spec_version == SplitIoClient::Spec::FeatureFlags::SPEC_VERSION
86
+ @clear_storage = @last_proxy_check_timestamp != 0
87
+ @last_proxy_check_timestamp = 0
88
+ end
89
+
90
+ unless result[:ff][:d].empty?
91
+ @config.split_logger.log_if_debug("#{result[:ff][:d].length} feature flags retrieved. since=#{since}")
92
+ end
93
+ @config.split_logger.log_if_transport("Feature flag changes response: #{result[:ff].to_s}")
94
+
95
+ unless result[:rbs][:d].empty?
96
+ @config.split_logger.log_if_debug("#{result[:rbs][:d].length} rule based segments retrieved. since=#{since_rbs}")
97
+ end
98
+ @config.split_logger.log_if_transport("rule based segments changes response: #{result[:rbs].to_s}")
99
+
100
+ bucket = BinarySearchLatencyTracker.get_bucket((Time.now - start) * 1000.0)
101
+ @telemetry_runtime_producer.record_sync_latency(Telemetry::Domain::Constants::SPLIT_SYNC, bucket)
102
+ @telemetry_runtime_producer.record_successful_sync(Telemetry::Domain::Constants::SPLIT_SYNC, (Time.now.to_f * 1000.0).to_i)
103
+
104
+ result
105
+ end
106
+
107
+ def check_rbs_data(rbs_data)
108
+ rbs_data.each do |rb_segment|
109
+ rb_segment[:excluded] = {:keys => [], :segments => []} if rb_segment[:excluded].nil?
110
+ rb_segment[:excluded][:keys] = [] if rb_segment[:excluded][:keys].nil?
111
+ rb_segment[:excluded][:segments] = [] if rb_segment[:excluded][:segments].nil?
112
+ end
113
+ rbs_data
114
+ end
53
115
 
54
- parsed_splits[:segment_names] =
55
- parsed_splits[:splits].each_with_object(Set.new) do |split, splits|
56
- splits << Helpers::Util.segment_names_by_feature_flag(split)
116
+ def objects_with_segment_names(parsed_objects)
117
+ parsed_objects[:segment_names] = Set.new
118
+ parsed_objects[:segment_names] =
119
+ parsed_objects[:ff][:d].each_with_object(Set.new) do |split, splits|
120
+ splits << Helpers::Util.segment_names_by_object(split, "IN_SEGMENT")
57
121
  end.flatten
58
122
 
59
- parsed_splits
123
+ parsed_objects[:rbs][:d].each do |rule_based_segment|
124
+ parsed_objects[:segment_names].merge Helpers::Util.segment_names_in_rb_segment(rule_based_segment, "IN_SEGMENT")
125
+ end
126
+
127
+ parsed_objects
128
+ end
129
+
130
+ def check_last_proxy_check_timestamp
131
+ @spec_version == Splits::SPEC_1_1 and ((Time.now - @last_proxy_check_timestamp) >= Splits::PROXY_CHECK_INTERVAL_SECONDS)
132
+ end
133
+
134
+ def convert_to_new_spec(body)
135
+ {:ff => {:d => body[:splits], :s => body[:since], :t => body[:till]}, :rbs => {:d => [], :s => -1, :t => -1}}
60
136
  end
61
137
  end
62
138
  end
@@ -21,10 +21,10 @@ module SplitIoClient
21
21
  return process_error(response) if response.status >= 400 && response.status < 500
22
22
 
23
23
  @telemetry_runtime_producer.record_sync_error(Telemetry::Domain::Constants::TOKEN_SYNC, response.status.to_i)
24
- @config.logger.debug("Error connecting to: #{@config.auth_service_url}. Response status: #{response.status}")
24
+ @config.logger.debug("Error connecting to: #{@config.auth_service_url}. Response status: #{response.status}") if @config.debug_enabled
25
25
  { push_enabled: false, retry: true }
26
26
  rescue StandardError => e
27
- @config.logger.debug("AuthApiClient error: #{e.inspect}.")
27
+ @config.logger.debug("AuthApiClient error: #{e.inspect}.") if @config.debug_enabled
28
28
  { push_enabled: false, retry: false }
29
29
  end
30
30
 
@@ -51,7 +51,7 @@ module SplitIoClient
51
51
  end
52
52
 
53
53
  def process_error(response)
54
- @config.logger.debug("Error connecting to: #{@config.auth_service_url}. Response status: #{response.status}")
54
+ @config.logger.debug("Error connecting to: #{@config.auth_service_url}. Response status: #{response.status}") if @config.debug_enabled
55
55
  @telemetry_runtime_producer.record_auth_rejections if response.status == 401
56
56
 
57
57
  { push_enabled: false, retry: false }
@@ -18,20 +18,24 @@ module SplitIoClient
18
18
  @unique_keys_tracker = unique_keys_tracker
19
19
  end
20
20
 
21
- def build_impression(matching_key, bucketing_key, split_name, treatment, params = {})
22
- impression_data = impression_data(matching_key, bucketing_key, split_name, treatment, params[:time])
21
+ def build_impression(matching_key, bucketing_key, split_name, treatment_data, impressions_disabled, params = {},
22
+ evaluation_options = nil)
23
+ properties = get_properties(evaluation_options)
24
+ impression_data = impression_data(matching_key, bucketing_key, split_name, treatment_data, params[:time], properties)
25
+ return impression(impression_data, params[:attributes]) if check_return_conditions(properties)
23
26
 
24
27
  begin
25
- case @config.impressions_mode
26
- when :debug # In DEBUG mode we should calculate the pt only.
27
- impression_data[:pt] = @impression_observer.test_and_set(impression_data)
28
- when :none # In NONE mode we should track the total amount of evaluations and the unique keys.
28
+ if check_none_mode(impressions_disabled)
29
29
  @impression_counter.inc(split_name, impression_data[:m])
30
30
  @unique_keys_tracker.track(split_name, matching_key)
31
- else # In OPTIMIZED mode we should track the total amount of evaluations and deduplicate the impressions.
31
+ end
32
+ if check_observe_impressions
33
+ # In DEBUG mode we should calculate the pt only.
32
34
  impression_data[:pt] = @impression_observer.test_and_set(impression_data)
33
-
34
- @impression_counter.inc(split_name, impression_data[:m]) unless impression_data[:pt].nil?
35
+ end
36
+ if check_impression_counter(impression_data)
37
+ # In OPTIMIZED mode we should track the total amount of evaluations and deduplicate the impressions.
38
+ @impression_counter.inc(split_name, impression_data[:m])
35
39
  end
36
40
  rescue StandardError => e
37
41
  @config.log_found_exception(__method__.to_s, e)
@@ -40,29 +44,50 @@ module SplitIoClient
40
44
  impression(impression_data, params[:attributes])
41
45
  end
42
46
 
43
- def track(impressions)
44
- return if impressions.empty?
45
-
46
- stats = { dropped: 0, queued: 0, dedupe: 0 }
47
- begin
48
- case @config.impressions_mode
49
- when :none
50
- return
51
- when :debug
52
- track_debug_mode(impressions, stats)
53
- when :optimized
54
- track_optimized_mode(impressions, stats)
47
+ def track(impressions_decorator)
48
+ return if impressions_decorator.empty?
49
+
50
+ impressions_decorator.each do |impression_decorator|
51
+ impression_router.add_bulk([impression_decorator[:impression]])
52
+ stats = { dropped: 0, queued: 0, dedupe: 0 }
53
+ begin
54
+ next if @config.impressions_mode == :none || impression_decorator[:disabled]
55
+
56
+ if @config.impressions_mode == :debug
57
+ track_debug_mode([impression_decorator[:impression]], stats)
58
+ else
59
+ track_optimized_mode([impression_decorator[:impression]], stats)
60
+ end
61
+ rescue StandardError => e
62
+ @config.log_found_exception(__method__.to_s, e)
63
+ ensure
64
+ record_stats(stats)
55
65
  end
56
- rescue StandardError => e
57
- @config.log_found_exception(__method__.to_s, e)
58
- ensure
59
- record_stats(stats)
60
- impression_router.add_bulk(impressions)
61
66
  end
62
67
  end
63
68
 
64
69
  private
65
70
 
71
+ def check_return_conditions(properties)
72
+ return (@config.impressions_mode == :debug || @config.impressions_mode == :optimized) && !properties.nil?
73
+ end
74
+
75
+ def check_none_mode(impressions_disabled)
76
+ return @config.impressions_mode == :none || impressions_disabled
77
+ end
78
+
79
+ def check_observe_impressions
80
+ return @config.impressions_mode == :debug || @config.impressions_mode == :optimized
81
+ end
82
+
83
+ def check_impression_counter(impression_data)
84
+ return @config.impressions_mode == :optimized && !impression_data[:pt].nil?
85
+ end
86
+
87
+ def get_properties(evaluation_options)
88
+ return evaluation_options.nil? ? nil : evaluation_options.properties
89
+ end
90
+
66
91
  def impression_router
67
92
  @impression_router ||= SplitIoClient::ImpressionRouter.new(@config)
68
93
  rescue StandardError => e
@@ -81,7 +106,8 @@ module SplitIoClient
81
106
  end
82
107
 
83
108
  # added param time for test
84
- def impression_data(matching_key, bucketing_key, split_name, treatment, time = nil)
109
+ def impression_data(matching_key, bucketing_key, split_name, treatment, time = nil,
110
+ properties = nil)
85
111
  {
86
112
  k: matching_key,
87
113
  b: bucketing_key,
@@ -90,7 +116,8 @@ module SplitIoClient
90
116
  r: applied_rule(treatment[:label]),
91
117
  c: treatment[:change_number],
92
118
  m: time || (Time.now.to_f * 1000.0).to_i,
93
- pt: nil
119
+ pt: nil,
120
+ properties: properties
94
121
  }
95
122
  end
96
123
 
@@ -126,11 +153,10 @@ module SplitIoClient
126
153
 
127
154
  def track_optimized_mode(impressions, stats)
128
155
  optimized_impressions = impressions.select { |imp| should_queue_impression?(imp[:i]) }
129
-
156
+ stats[:dedupe] = impressions.length - optimized_impressions.length
130
157
  return if optimized_impressions.empty?
131
158
 
132
159
  stats[:dropped] = @impressions_repository.add_bulk(optimized_impressions)
133
- stats[:dedupe] = impressions.length - optimized_impressions.length
134
160
  stats[:queued] = optimized_impressions.length - stats[:dropped]
135
161
  end
136
162
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Engine
5
+ module Events
6
+ class EventsDelivery
7
+ def initialize(config)
8
+ @config = config
9
+ end
10
+
11
+ def deliver(sdk_event, event_metadata, event_handler)
12
+ event_handler.call(event_metadata)
13
+ rescue StandardError => e
14
+ @config.logger.error("Exception when calling handler for Sdk Event #{sdk_event}")
15
+ @config.log_found_exception(__method__.to_s, e)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Engine
5
+ module Events
6
+ class EventsManager
7
+ def initialize(events_manager_config, events_delivery, config)
8
+ @manager_config = events_manager_config
9
+ @events_delivery = events_delivery
10
+ @active_subscriptions = {}
11
+ @internal_events_status = {}
12
+ @mutex = Mutex.new
13
+ @config = config
14
+ end
15
+
16
+ def register(sdk_event, event_handler)
17
+ return if @active_subscriptions.key?(sdk_event) && !get_event_handler(sdk_event).nil?
18
+
19
+ @mutex.synchronize do
20
+ # SDK ready already fired
21
+ if sdk_event == SplitIoClient::Engine::Models::SdkEvent::SDK_READY && event_already_triggered(sdk_event)
22
+ @active_subscriptions[sdk_event] = SplitIoClient::Engine::Models::EventActiveSubscriptions.new(true, event_handler)
23
+ @config.logger.debug('EventsManager: Firing SDK_READY event for new subscription') if @config.debug_enabled
24
+ fire_sdk_event(sdk_event, nil)
25
+ return
26
+ end
27
+
28
+ @config.logger.debug("EventsManager: Register event: #{sdk_event}") if @config.debug_enabled
29
+ @active_subscriptions[sdk_event] = SplitIoClient::Engine::Models::EventActiveSubscriptions.new(false, event_handler)
30
+ end
31
+ end
32
+
33
+ def unregister(sdk_event)
34
+ return unless @active_subscriptions.key?(sdk_event)
35
+
36
+ @mutex.synchronize do
37
+ @active_subscriptions.delete(sdk_event)
38
+ end
39
+ end
40
+
41
+ def notify_internal_event(sdk_internal_event, event_metadata)
42
+ @mutex.synchronize do
43
+ update_internal_event_status(sdk_internal_event, true)
44
+ @manager_config.evaluation_order.each do |sorted_event|
45
+ if get_sdk_event_if_applicable(sdk_internal_event).include?(sorted_event) &&
46
+ !get_event_handler(sorted_event).nil?
47
+ fire_sdk_event(sorted_event, event_metadata)
48
+ end
49
+
50
+ # if client is not subscribed to SDK_READY
51
+ if check_if_register_needed(sorted_event)
52
+ @config.logger.debug('EventsManager: Registering SDK_READY event as fired') if @config.debug_enabled
53
+ @active_subscriptions[Engine::Models::SdkEvent::SDK_READY] = Engine::Models::EventActiveSubscriptions.new(true, nil)
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def destroy
60
+ @mutex.synchronize do
61
+ @active_subscriptions = {}
62
+ @internal_events_status = {}
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def check_if_register_needed(sorted_event)
69
+ sorted_event == SplitIoClient::Engine::Models::SdkEvent::SDK_READY &&
70
+ get_event_handler(sorted_event).nil? &&
71
+ !@active_subscriptions.include?(sorted_event)
72
+ end
73
+
74
+ def fire_sdk_event(sdk_event, event_metadata)
75
+ @config.logger.debug("EventsManager: Firing Sdk event: #{sdk_event}") if @config.debug_enabled
76
+ @config.threads[:sdk_event_notify] = Thread.new do
77
+ @events_delivery.deliver(sdk_event, event_metadata, get_event_handler(sdk_event))
78
+ end
79
+ sdk_event_triggered(sdk_event)
80
+ end
81
+
82
+ def event_already_triggered(sdk_event)
83
+ return @active_subscriptions[sdk_event].triggered if @active_subscriptions.key?(sdk_event)
84
+
85
+ false
86
+ end
87
+
88
+ def get_internal_event_status(sdk_internal_event)
89
+ return @internal_events_status[sdk_internal_event] if @internal_events_status.key?(sdk_internal_event)
90
+
91
+ false
92
+ end
93
+
94
+ def update_internal_event_status(sdk_internal_event, status)
95
+ @internal_events_status[sdk_internal_event] = status
96
+ end
97
+
98
+ def sdk_event_triggered(sdk_event)
99
+ return unless @active_subscriptions.key?(sdk_event)
100
+
101
+ return if @active_subscriptions[sdk_event].triggered
102
+
103
+ @active_subscriptions[sdk_event].triggered = true
104
+ end
105
+
106
+ def get_event_handler(sdk_event)
107
+ return nil unless @active_subscriptions.key?(sdk_event)
108
+
109
+ @active_subscriptions[sdk_event].handler
110
+ end
111
+
112
+ def get_sdk_event_if_applicable(sdk_internal_event)
113
+ final_sdk_event = SplitIoClient::Engine::Models::ValidSdkEvent.new(nil, false)
114
+
115
+ events_to_fire = []
116
+ require_any_sdk_event = check_require_any(sdk_internal_event)
117
+ if require_any_sdk_event.valid
118
+ if (!event_already_triggered(require_any_sdk_event.sdk_event) &&
119
+ execution_limit(require_any_sdk_event.sdk_event) == 1) ||
120
+ execution_limit(require_any_sdk_event.sdk_event) == -1
121
+ final_sdk_event = SplitIoClient::Engine::Models::ValidSdkEvent.new(
122
+ require_any_sdk_event.sdk_event,
123
+ check_prerequisites(require_any_sdk_event.sdk_event) &&
124
+ check_suppressed_by(require_any_sdk_event.sdk_event)
125
+ )
126
+ end
127
+ events_to_fire.push(final_sdk_event.sdk_event) if final_sdk_event.valid
128
+ end
129
+ check_require_all.each { |sdk_event| events_to_fire.push(sdk_event) }
130
+
131
+ events_to_fire
132
+ end
133
+
134
+ def check_require_all
135
+ events = []
136
+ @manager_config.require_all.each do |require_name, require_value|
137
+ final_status = true
138
+ require_value.each { |val| final_status &= get_internal_event_status(val) }
139
+ events.push(require_name) if check_event_eligible_conditions(final_status, require_name, require_value)
140
+ end
141
+
142
+ events
143
+ end
144
+
145
+ def check_event_eligible_conditions(final_status, require_name, require_value)
146
+ final_status &&
147
+ check_prerequisites(require_name) &&
148
+ ((!event_already_triggered(require_name) &&
149
+ execution_limit(require_name) == 1) ||
150
+ execution_limit(require_name) == -1) &&
151
+ require_value.length.positive?
152
+ end
153
+
154
+ def check_prerequisites(sdk_event)
155
+ @manager_config.prerequisites.each do |name, value|
156
+ value.each do |val|
157
+ return false if name == sdk_event && !event_already_triggered(val)
158
+ end
159
+ end
160
+
161
+ true
162
+ end
163
+
164
+ def check_suppressed_by(sdk_event)
165
+ @manager_config.suppressed_by.each do |name, value|
166
+ value.each do |val|
167
+ return false if name == sdk_event && event_already_triggered(val)
168
+ end
169
+ end
170
+
171
+ true
172
+ end
173
+
174
+ def execution_limit(sdk_event)
175
+ return -1 unless @manager_config.execution_limits.key?(sdk_event)
176
+
177
+ @manager_config.execution_limits[sdk_event]
178
+ end
179
+
180
+ def check_require_any(sdk_internal_event)
181
+ valid_sdk_event = SplitIoClient::Engine::Models::ValidSdkEvent.new(nil, false)
182
+ @manager_config.require_any.each do |name, val|
183
+ if val.include?(sdk_internal_event)
184
+ valid_sdk_event = SplitIoClient::Engine::Models::ValidSdkEvent.new(name, true)
185
+ return valid_sdk_event
186
+ end
187
+ end
188
+
189
+ valid_sdk_event
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Engine
5
+ module Events
6
+ class EventsManagerConfig
7
+ attr_accessor :require_all, :prerequisites, :require_any, :suppressed_by, :execution_limits, :evaluation_order
8
+
9
+ def initialize
10
+ @require_all = construct_require_all
11
+ @prerequisites = construct_prerequisites
12
+ @require_any = construct_require_any
13
+ @suppressed_by = construct_suppressed_by
14
+ @execution_limits = construct_execution_limits
15
+ @evaluation_order = construct_sorted_events
16
+ end
17
+
18
+ private
19
+
20
+ def construct_require_all
21
+ {
22
+ SplitIoClient::Engine::Models::SdkEvent::SDK_READY => Set.new([SplitIoClient::Engine::Models::SdkInternalEvent::SDK_READY])
23
+ }
24
+ end
25
+
26
+ def construct_prerequisites
27
+ {
28
+ SplitIoClient::Engine::Models::SdkEvent::SDK_UPDATE => Set.new([SplitIoClient::Engine::Models::SdkEvent::SDK_READY])
29
+ }
30
+ end
31
+
32
+ def construct_require_any
33
+ {
34
+ SplitIoClient::Engine::Models::SdkEvent::SDK_UPDATE => Set.new(
35
+ [
36
+ SplitIoClient::Engine::Models::SdkInternalEvent::FLAG_KILLED_NOTIFICATION,
37
+ SplitIoClient::Engine::Models::SdkInternalEvent::FLAGS_UPDATED,
38
+ SplitIoClient::Engine::Models::SdkInternalEvent::RB_SEGMENTS_UPDATED,
39
+ SplitIoClient::Engine::Models::SdkInternalEvent::SEGMENTS_UPDATED
40
+ ]
41
+ )
42
+ }
43
+ end
44
+
45
+ def construct_suppressed_by
46
+ {}
47
+ end
48
+
49
+ def construct_execution_limits
50
+ {
51
+ SplitIoClient::Engine::Models::SdkEvent::SDK_READY => 1,
52
+ SplitIoClient::Engine::Models::SdkEvent::SDK_UPDATE => -1
53
+ }
54
+ end
55
+
56
+ def construct_sorted_events
57
+ sorted_events = []
58
+ [SplitIoClient::Engine::Models::SdkEvent::SDK_READY, SplitIoClient::Engine::Models::SdkEvent::SDK_UPDATE].each do |sdk_event|
59
+ sorted_events = dfs_recursive(sdk_event, sorted_events)
60
+ end
61
+
62
+ sorted_events
63
+ end
64
+
65
+ def dfs_recursive(sdk_event, added)
66
+ return added if added.include?(sdk_event)
67
+
68
+ get_dependencies(sdk_event).each do |dependent_event|
69
+ added = dfs_recursive(dependent_event, added)
70
+ end
71
+
72
+ added.push(sdk_event)
73
+
74
+ added
75
+ end
76
+
77
+ def get_dependencies(sdk_event)
78
+ dependencies = Set.new
79
+ @prerequisites.each do |prerequisites_event_name, prerequisites_event_value|
80
+ next unless prerequisites_event_name == sdk_event
81
+
82
+ prerequisites_event_value.each do |prereq_event|
83
+ dependencies.add(prereq_event)
84
+ end
85
+ end
86
+
87
+ @suppressed_by.each do |suppressed_event_name, suppressed_event_value|
88
+ dependencies.add(suppressed_event_name) if suppressed_event_value.include?(sdk_event)
89
+ end
90
+
91
+ dependencies
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Engine
5
+ module Events
6
+ class EventsTask
7
+ attr_accessor :running
8
+
9
+ def initialize(notify_internal_events, internal_events_queue, config)
10
+ @notify_internal_events = notify_internal_events
11
+ @internal_events_queue = internal_events_queue
12
+ @config = config
13
+ @running = false
14
+ end
15
+
16
+ def start
17
+ return if @running
18
+
19
+ @config.logger.info('Starting Internal Events Task.')
20
+ @running = true
21
+ @config.threads[:internal_events_task] = Thread.new do
22
+ worker_thread
23
+ end
24
+ end
25
+
26
+ def stop
27
+ return unless @running
28
+
29
+ @config.logger.info('Stopping Internal Events Task.')
30
+ @running = false
31
+ end
32
+
33
+ private
34
+
35
+ def worker_thread
36
+ while (event = @internal_events_queue.pop)
37
+ break unless @running
38
+
39
+ @config.logger.debug("Processing sdk internal event: #{event.internal_event}") if @config.debug_enabled
40
+ begin
41
+ @notify_internal_events.call(event.internal_event, event.metadata)
42
+ rescue StandardError => e
43
+ @config.log_found_exception(__method__.to_s, e)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end