splitclient-rb 6.3.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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/pull_request_template.md +9 -0
  4. data/.github/workflows/ci.yml +90 -0
  5. data/.github/workflows/update-license-year.yml +45 -0
  6. data/.gitignore +4 -0
  7. data/.rubocop.yml +46 -3
  8. data/CHANGES.txt +158 -11
  9. data/CONTRIBUTORS-GUIDE.md +49 -0
  10. data/LICENSE +169 -13
  11. data/NOTICE.txt +5 -0
  12. data/README.md +67 -27
  13. data/Rakefile +1 -8
  14. data/ext/murmurhash/3_x64_128.c +117 -0
  15. data/ext/murmurhash/murmurhash.c +5 -1
  16. data/lib/murmurhash/murmurhash.jar +0 -0
  17. data/lib/splitclient-rb/cache/adapters/cache_adapter.rb +3 -3
  18. data/lib/splitclient-rb/cache/adapters/memory_adapters/map_adapter.rb +4 -0
  19. data/lib/splitclient-rb/cache/adapters/memory_adapters/queue_adapter.rb +7 -0
  20. data/lib/splitclient-rb/cache/adapters/redis_adapter.rb +12 -4
  21. data/lib/splitclient-rb/cache/fetchers/segment_fetcher.rb +83 -0
  22. data/lib/splitclient-rb/cache/fetchers/split_fetcher.rb +70 -0
  23. data/lib/splitclient-rb/cache/filter/bloom_filter.rb +67 -0
  24. data/lib/splitclient-rb/cache/filter/filter_adapter.rb +32 -0
  25. data/lib/splitclient-rb/cache/filter/flag_set_filter.rb +40 -0
  26. data/lib/splitclient-rb/cache/hashers/impression_hasher.rb +34 -0
  27. data/lib/splitclient-rb/cache/observers/impression_observer.rb +22 -0
  28. data/lib/splitclient-rb/cache/observers/noop_impression_observer.rb +10 -0
  29. data/lib/splitclient-rb/cache/repositories/events/memory_repository.rb +26 -14
  30. data/lib/splitclient-rb/cache/repositories/events/redis_repository.rb +9 -14
  31. data/lib/splitclient-rb/cache/repositories/events_repository.rb +31 -10
  32. data/lib/splitclient-rb/cache/repositories/flag_sets/memory_repository.rb +40 -0
  33. data/lib/splitclient-rb/cache/repositories/flag_sets/redis_repository.rb +49 -0
  34. data/lib/splitclient-rb/cache/repositories/impressions/memory_repository.rb +22 -23
  35. data/lib/splitclient-rb/cache/repositories/impressions/redis_repository.rb +15 -22
  36. data/lib/splitclient-rb/cache/repositories/impressions_repository.rb +6 -31
  37. data/lib/splitclient-rb/cache/repositories/repository.rb +6 -5
  38. data/lib/splitclient-rb/cache/repositories/rule_based_segments_repository.rb +136 -0
  39. data/lib/splitclient-rb/cache/repositories/segments_repository.rb +46 -6
  40. data/lib/splitclient-rb/cache/repositories/splits_repository.rb +232 -43
  41. data/lib/splitclient-rb/cache/routers/impression_router.rb +24 -22
  42. data/lib/splitclient-rb/cache/senders/events_sender.rb +12 -29
  43. data/lib/splitclient-rb/cache/senders/impressions_adapter/memory_sender.rb +71 -0
  44. data/lib/splitclient-rb/cache/senders/impressions_adapter/redis_sender.rb +69 -0
  45. data/lib/splitclient-rb/cache/senders/impressions_count_sender.rb +43 -0
  46. data/lib/splitclient-rb/cache/senders/impressions_formatter.rb +27 -13
  47. data/lib/splitclient-rb/cache/senders/impressions_sender.rb +11 -25
  48. data/lib/splitclient-rb/cache/senders/impressions_sender_adapter.rb +21 -0
  49. data/lib/splitclient-rb/cache/senders/localhost_repo_cleaner.rb +47 -0
  50. data/lib/splitclient-rb/cache/stores/localhost_split_builder.rb +95 -0
  51. data/lib/splitclient-rb/cache/stores/localhost_split_store.rb +110 -0
  52. data/lib/splitclient-rb/cache/stores/store_utils.rb +13 -0
  53. data/lib/splitclient-rb/clients/split_client.rb +385 -138
  54. data/lib/splitclient-rb/constants.rb +16 -0
  55. data/lib/splitclient-rb/engine/api/client.rb +38 -43
  56. data/lib/splitclient-rb/engine/api/events.rb +19 -11
  57. data/lib/splitclient-rb/engine/api/faraday_middleware/gzip.rb +1 -0
  58. data/lib/splitclient-rb/engine/api/impressions.rb +49 -14
  59. data/lib/splitclient-rb/engine/api/segments.rb +31 -24
  60. data/lib/splitclient-rb/engine/api/splits.rb +108 -33
  61. data/lib/splitclient-rb/engine/api/telemetry_api.rb +47 -0
  62. data/lib/splitclient-rb/engine/auth_api_client.rb +96 -0
  63. data/lib/splitclient-rb/engine/back_off.rb +26 -0
  64. data/lib/splitclient-rb/engine/common/impressions_counter.rb +45 -0
  65. data/lib/splitclient-rb/engine/common/impressions_manager.rb +165 -0
  66. data/lib/splitclient-rb/engine/common/noop_impressions_counter.rb +27 -0
  67. data/lib/splitclient-rb/engine/events/events_delivery.rb +20 -0
  68. data/lib/splitclient-rb/engine/events/events_manager.rb +194 -0
  69. data/lib/splitclient-rb/engine/events/events_manager_config.rb +96 -0
  70. data/lib/splitclient-rb/engine/events/events_task.rb +50 -0
  71. data/lib/splitclient-rb/engine/events/noop_events_queue.rb +13 -0
  72. data/lib/splitclient-rb/engine/fallback_treatment_calculator.rb +48 -0
  73. data/lib/splitclient-rb/engine/impressions/noop_unique_keys_tracker.rb +17 -0
  74. data/lib/splitclient-rb/engine/impressions/unique_keys_tracker.rb +144 -0
  75. data/lib/splitclient-rb/engine/matchers/all_keys_matcher.rb +1 -1
  76. data/lib/splitclient-rb/engine/matchers/between_matcher.rb +7 -5
  77. data/lib/splitclient-rb/engine/matchers/between_semver_matcher.rb +33 -0
  78. data/lib/splitclient-rb/engine/matchers/combining_matcher.rb +10 -8
  79. data/lib/splitclient-rb/engine/matchers/contains_all_matcher.rb +2 -6
  80. data/lib/splitclient-rb/engine/matchers/contains_any_matcher.rb +1 -5
  81. data/lib/splitclient-rb/engine/matchers/contains_matcher.rb +7 -5
  82. data/lib/splitclient-rb/engine/matchers/dependency_matcher.rb +6 -5
  83. data/lib/splitclient-rb/engine/matchers/ends_with_matcher.rb +5 -4
  84. data/lib/splitclient-rb/engine/matchers/equal_to_boolean_matcher.rb +3 -2
  85. data/lib/splitclient-rb/engine/matchers/equal_to_matcher.rb +6 -4
  86. data/lib/splitclient-rb/engine/matchers/equal_to_semver_matcher.rb +28 -0
  87. data/lib/splitclient-rb/engine/matchers/equal_to_set_matcher.rb +1 -5
  88. data/lib/splitclient-rb/engine/matchers/greater_than_or_equal_to_matcher.rb +6 -4
  89. data/lib/splitclient-rb/engine/matchers/greater_than_or_equal_to_semver_matcher.rb +28 -0
  90. data/lib/splitclient-rb/engine/matchers/in_list_semver_matcher.rb +36 -0
  91. data/lib/splitclient-rb/engine/matchers/less_than_or_equal_to_matcher.rb +6 -4
  92. data/lib/splitclient-rb/engine/matchers/less_than_or_equal_to_semver_matcher.rb +28 -0
  93. data/lib/splitclient-rb/engine/matchers/matcher.rb +22 -0
  94. data/lib/splitclient-rb/engine/matchers/matches_string_matcher.rb +3 -2
  95. data/lib/splitclient-rb/engine/matchers/negation_matcher.rb +3 -2
  96. data/lib/splitclient-rb/engine/matchers/part_of_set_matcher.rb +2 -6
  97. data/lib/splitclient-rb/engine/matchers/prerequisites_matcher.rb +31 -0
  98. data/lib/splitclient-rb/engine/matchers/rule_based_segment_matcher.rb +78 -0
  99. data/lib/splitclient-rb/engine/matchers/semver.rb +201 -0
  100. data/lib/splitclient-rb/engine/matchers/set_matcher.rb +2 -1
  101. data/lib/splitclient-rb/engine/matchers/starts_with_matcher.rb +4 -3
  102. data/lib/splitclient-rb/engine/matchers/user_defined_segment_matcher.rb +3 -2
  103. data/lib/splitclient-rb/engine/matchers/whitelist_matcher.rb +7 -5
  104. data/lib/splitclient-rb/engine/metrics/binary_search_latency_tracker.rb +3 -65
  105. data/lib/splitclient-rb/engine/models/evaluation_options.rb +9 -0
  106. data/lib/splitclient-rb/engine/models/event_active_subscriptions.rb +14 -0
  107. data/lib/splitclient-rb/engine/models/events_metadata.rb +10 -0
  108. data/lib/splitclient-rb/engine/models/fallback_treatment.rb +11 -0
  109. data/lib/splitclient-rb/engine/models/fallback_treatments_configuration.rb +36 -0
  110. data/lib/splitclient-rb/engine/models/label.rb +3 -0
  111. data/lib/splitclient-rb/engine/models/sdk_event.rb +4 -0
  112. data/lib/splitclient-rb/engine/models/sdk_event_type.rb +4 -0
  113. data/lib/splitclient-rb/engine/models/sdk_internal_event.rb +8 -0
  114. data/lib/splitclient-rb/engine/models/sdk_internal_event_notification.rb +14 -0
  115. data/lib/splitclient-rb/engine/models/segment_type.rb +4 -0
  116. data/lib/splitclient-rb/engine/models/split_http_response.rb +19 -0
  117. data/lib/splitclient-rb/engine/models/valid_sdk_event.rb +14 -0
  118. data/lib/splitclient-rb/engine/parser/condition.rb +81 -20
  119. data/lib/splitclient-rb/engine/parser/evaluator.rb +40 -51
  120. data/lib/splitclient-rb/engine/push_manager.rb +66 -0
  121. data/lib/splitclient-rb/engine/status_manager.rb +39 -0
  122. data/lib/splitclient-rb/engine/sync_manager.rb +180 -0
  123. data/lib/splitclient-rb/engine/synchronizer.rb +231 -0
  124. data/lib/splitclient-rb/exceptions.rb +20 -1
  125. data/lib/splitclient-rb/helpers/decryption_helper.rb +25 -0
  126. data/lib/splitclient-rb/helpers/evaluator_helper.rb +37 -0
  127. data/lib/splitclient-rb/helpers/repository_helper.rb +61 -0
  128. data/lib/splitclient-rb/helpers/thread_helper.rb +24 -0
  129. data/lib/splitclient-rb/helpers/util.rb +26 -0
  130. data/lib/splitclient-rb/managers/split_manager.rb +58 -20
  131. data/lib/splitclient-rb/spec.rb +9 -0
  132. data/lib/splitclient-rb/split_config.rb +336 -54
  133. data/lib/splitclient-rb/split_factory.rb +219 -33
  134. data/lib/splitclient-rb/split_factory_builder.rb +1 -22
  135. data/lib/splitclient-rb/split_factory_registry.rb +63 -0
  136. data/lib/splitclient-rb/split_logger.rb +9 -10
  137. data/lib/splitclient-rb/sse/event_source/client.rb +263 -0
  138. data/lib/splitclient-rb/sse/event_source/event_parser.rb +65 -0
  139. data/lib/splitclient-rb/sse/event_source/event_types.rb +15 -0
  140. data/lib/splitclient-rb/sse/event_source/stream_data.rb +22 -0
  141. data/lib/splitclient-rb/sse/notification_manager_keeper.rb +84 -0
  142. data/lib/splitclient-rb/sse/notification_processor.rb +48 -0
  143. data/lib/splitclient-rb/sse/sse_handler.rb +44 -0
  144. data/lib/splitclient-rb/sse/workers/segments_worker.rb +62 -0
  145. data/lib/splitclient-rb/sse/workers/splits_worker.rb +149 -0
  146. data/lib/splitclient-rb/telemetry/domain/constants.rb +48 -0
  147. data/lib/splitclient-rb/telemetry/domain/structs.rb +35 -0
  148. data/lib/splitclient-rb/telemetry/evaluation_consumer.rb +14 -0
  149. data/lib/splitclient-rb/telemetry/evaluation_producer.rb +21 -0
  150. data/lib/splitclient-rb/telemetry/init_consumer.rb +14 -0
  151. data/lib/splitclient-rb/telemetry/init_producer.rb +19 -0
  152. data/lib/splitclient-rb/telemetry/memory/memory_evaluation_consumer.rb +32 -0
  153. data/lib/splitclient-rb/telemetry/memory/memory_evaluation_producer.rb +24 -0
  154. data/lib/splitclient-rb/telemetry/memory/memory_init_consumer.rb +28 -0
  155. data/lib/splitclient-rb/telemetry/memory/memory_init_producer.rb +34 -0
  156. data/lib/splitclient-rb/telemetry/memory/memory_runtime_consumer.rb +119 -0
  157. data/lib/splitclient-rb/telemetry/memory/memory_runtime_producer.rb +87 -0
  158. data/lib/splitclient-rb/telemetry/memory/memory_synchronizer.rb +213 -0
  159. data/lib/splitclient-rb/telemetry/redis/redis_evaluation_producer.rb +38 -0
  160. data/lib/splitclient-rb/telemetry/redis/redis_init_producer.rb +37 -0
  161. data/lib/splitclient-rb/telemetry/redis/redis_synchronizer.rb +27 -0
  162. data/lib/splitclient-rb/telemetry/runtime_consumer.rb +25 -0
  163. data/lib/splitclient-rb/telemetry/runtime_producer.rb +25 -0
  164. data/lib/splitclient-rb/telemetry/storages/memory.rb +159 -0
  165. data/lib/splitclient-rb/telemetry/sync_task.rb +36 -0
  166. data/lib/splitclient-rb/telemetry/synchronizer.rb +33 -0
  167. data/lib/splitclient-rb/utilitites.rb +8 -0
  168. data/lib/splitclient-rb/validators.rb +142 -38
  169. data/lib/splitclient-rb/version.rb +1 -1
  170. data/lib/splitclient-rb.rb +101 -16
  171. data/sonar-project.properties +6 -0
  172. data/splitclient-rb.gemspec +28 -23
  173. metadata +262 -82
  174. data/.travis.yml +0 -11
  175. data/Appraisals +0 -10
  176. data/Detailed-README.md +0 -588
  177. data/NEWS +0 -141
  178. data/lib/splitclient-rb/cache/repositories/metrics/memory_repository.rb +0 -127
  179. data/lib/splitclient-rb/cache/repositories/metrics/redis_repository.rb +0 -96
  180. data/lib/splitclient-rb/cache/repositories/metrics_repository.rb +0 -21
  181. data/lib/splitclient-rb/cache/senders/metrics_sender.rb +0 -56
  182. data/lib/splitclient-rb/cache/stores/sdk_blocker.rb +0 -46
  183. data/lib/splitclient-rb/cache/stores/segment_store.rb +0 -81
  184. data/lib/splitclient-rb/cache/stores/split_store.rb +0 -102
  185. data/lib/splitclient-rb/clients/localhost_split_client.rb +0 -183
  186. data/lib/splitclient-rb/engine/api/faraday_adapter/patched_net_http_persistent.rb +0 -46
  187. data/lib/splitclient-rb/engine/api/metrics.rb +0 -60
  188. data/lib/splitclient-rb/engine/metrics/metrics.rb +0 -80
  189. data/lib/splitclient-rb/engine/parser/split_adapter.rb +0 -81
  190. data/lib/splitclient-rb/localhost_split_factory.rb +0 -13
  191. data/lib/splitclient-rb/localhost_utils.rb +0 -59
  192. data/lib/splitclient-rb/managers/localhost_split_manager.rb +0 -60
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Engine
5
+ class PushManager
6
+ def initialize(config, sse_handler, api_key, telemetry_runtime_producer)
7
+ @config = config
8
+ @sse_handler = sse_handler
9
+ @auth_api_client = AuthApiClient.new(@config, telemetry_runtime_producer)
10
+ @api_key = api_key
11
+ @back_off = Engine::BackOff.new(@config.auth_retry_back_off_base, 1)
12
+ @telemetry_runtime_producer = telemetry_runtime_producer
13
+ end
14
+
15
+ def start_sse
16
+ response = @auth_api_client.authenticate(@api_key)
17
+ @config.logger.debug("Auth service response push_enabled: #{response[:push_enabled]}") if @config.debug_enabled
18
+
19
+ if response[:push_enabled] && @sse_handler.start(response[:token], response[:channels])
20
+ schedule_next_token_refresh(response[:exp])
21
+ @back_off.reset
22
+ record_telemetry(response[:exp])
23
+
24
+ return true
25
+ end
26
+
27
+ stop_sse
28
+ schedule_next_token_refresh(@back_off.interval) if response[:retry]
29
+ false
30
+ rescue StandardError => e
31
+ @config.logger.error("start_sse: #{e.inspect}")
32
+ end
33
+
34
+ def stop_sse
35
+ @sse_handler.stop
36
+ SplitIoClient::Helpers::ThreadHelper.stop(:schedule_next_token_refresh, @config)
37
+ rescue StandardError => e
38
+ @config.logger.error(e.inspect)
39
+ end
40
+
41
+ private
42
+
43
+ def schedule_next_token_refresh(time)
44
+ @config.threads[:schedule_next_token_refresh] = Thread.new { refresh_token_task(time) }
45
+ end
46
+
47
+ def refresh_token_task(time)
48
+ @config.logger.debug("schedule_next_token_refresh refresh in #{time} seconds.") if @config.debug_enabled
49
+
50
+ sleep(time)
51
+
52
+ @config.logger.debug('schedule_next_token_refresh starting ...') if @config.debug_enabled
53
+ @sse_handler.stop
54
+
55
+ start_sse
56
+ rescue StandardError => e
57
+ @config.logger.debug("schedule_next_token_refresh error: #{e.inspect}") if @config.debug_enabled
58
+ end
59
+
60
+ def record_telemetry(time)
61
+ data = (Time.now.to_f * 1000.0).to_i + (time * 1000.0).to_i
62
+ @telemetry_runtime_producer.record_streaming_event(Telemetry::Domain::Constants::TOKEN_REFRESH, data)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Engine
5
+ class StatusManager
6
+ def initialize(config, internal_events_queue)
7
+ @config = config
8
+ @sdk_ready = Concurrent::CountDownLatch.new(1)
9
+ @internal_events_queue = internal_events_queue
10
+ end
11
+
12
+ def ready?
13
+ return true if @config.consumer?
14
+
15
+ @sdk_ready.wait(0)
16
+ end
17
+
18
+ def ready!
19
+ return if ready?
20
+
21
+ @sdk_ready.count_down
22
+ @config.logger.info('SplitIO SDK is ready')
23
+ @internal_events_queue.push(
24
+ SplitIoClient::Engine::Models::SdkInternalEventNotification.new(
25
+ SplitIoClient::Engine::Models::SdkInternalEvent::SDK_READY, nil
26
+ )
27
+ )
28
+ end
29
+
30
+ def wait_until_ready(seconds = nil)
31
+ return if @config.consumer?
32
+
33
+ timeout = seconds || @config.block_until_ready
34
+
35
+ raise SDKBlockerTimeoutExpiredException, 'SDK start up timeout expired' unless @sdk_ready.wait(timeout)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Engine
5
+ class SyncManager
6
+ SYNC_MODE_STREAMING = 0
7
+ SYNC_MODE_POLLING = 1
8
+
9
+ def initialize(config,
10
+ synchronizer,
11
+ telemetry_runtime_producer,
12
+ telemetry_synchronizer,
13
+ status_manager,
14
+ sse_handler,
15
+ push_manager,
16
+ status_queue)
17
+ @config = config
18
+ @synchronizer = synchronizer
19
+ @telemetry_runtime_producer = telemetry_runtime_producer
20
+ @telemetry_synchronizer = telemetry_synchronizer
21
+ @status_manager = status_manager
22
+ @sse_handler = sse_handler
23
+ @push_manager = push_manager
24
+ @status_queue = status_queue
25
+ @sse_connected = Concurrent::AtomicBoolean.new(false)
26
+ end
27
+
28
+ def start
29
+ start_thread
30
+ PhusionPassenger.on_event(:starting_worker_process) { |forked| start_thread if forked } if defined?(PhusionPassenger)
31
+ end
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
+
38
+ private
39
+
40
+ def start_thread
41
+ @config.threads[:start_sdk] = Thread.new do
42
+ sleep(0.5) until @synchronizer.sync_all(false)
43
+
44
+ @status_manager.ready!
45
+ @telemetry_synchronizer.synchronize_config
46
+ @synchronizer.start_periodic_data_recording
47
+ connected = false
48
+
49
+ if @config.streaming_enabled
50
+ @config.logger.debug('Starting Streaming mode ...') if @config.debug_enabled
51
+ start_push_status_monitor
52
+ connected = @push_manager.start_sse
53
+ end
54
+
55
+ unless connected
56
+ @config.logger.debug('Starting Polling mode ...') if @config.debug_enabled
57
+ @synchronizer.start_periodic_fetch
58
+ record_telemetry(Telemetry::Domain::Constants::SYNC_MODE, SYNC_MODE_POLLING)
59
+ end
60
+ end
61
+ end
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
+
71
+ def process_subsystem_ready
72
+ @synchronizer.stop_periodic_fetch
73
+ @synchronizer.sync_all
74
+ @sse_handler.start_workers
75
+ record_telemetry(Telemetry::Domain::Constants::SYNC_MODE, SYNC_MODE_STREAMING)
76
+ end
77
+
78
+ def process_subsystem_down
79
+ @sse_handler.stop_workers
80
+ @synchronizer.start_periodic_fetch
81
+ record_telemetry(Telemetry::Domain::Constants::SYNC_MODE, SYNC_MODE_POLLING)
82
+ end
83
+
84
+ def process_push_shutdown
85
+ @push_manager.stop_sse
86
+ @sse_handler.stop_workers
87
+ @synchronizer.start_periodic_fetch
88
+ record_telemetry(Telemetry::Domain::Constants::SYNC_MODE, SYNC_MODE_POLLING)
89
+ rescue StandardError => e
90
+ @config.logger.error("process_push_shutdown error: #{e.inspect}")
91
+ end
92
+
93
+ def process_connected
94
+ if @sse_connected.value
95
+ @config.logger.debug('Streaming already connected.') if @config.debug_enabled
96
+ return
97
+ end
98
+
99
+ @sse_connected.make_true
100
+ @synchronizer.stop_periodic_fetch
101
+ @synchronizer.sync_all
102
+ @sse_handler.start_workers
103
+ record_telemetry(Telemetry::Domain::Constants::SYNC_MODE, SYNC_MODE_STREAMING)
104
+ rescue StandardError => e
105
+ @config.logger.error("process_connected error: #{e.inspect}")
106
+ end
107
+
108
+ def process_forced_stop
109
+ unless @sse_connected.value
110
+ @config.logger.debug('Streaming already disconnected.') if @config.debug_enabled
111
+ return
112
+ end
113
+
114
+ @sse_connected.make_false
115
+ @synchronizer.start_periodic_fetch
116
+ record_telemetry(Telemetry::Domain::Constants::SYNC_MODE, SYNC_MODE_POLLING)
117
+ rescue StandardError => e
118
+ @config.logger.error("process_connected error: #{e.inspect}")
119
+ end
120
+
121
+ def process_disconnect(reconnect)
122
+ unless @sse_connected.value
123
+ @config.logger.debug('Streaming already disconnected.') if @config.debug_enabled
124
+ return
125
+ end
126
+
127
+ @sse_connected.make_false
128
+ @sse_handler.stop_workers
129
+ @synchronizer.start_periodic_fetch
130
+ record_telemetry(Telemetry::Domain::Constants::SYNC_MODE, SYNC_MODE_POLLING)
131
+
132
+ if reconnect
133
+ @push_manager.stop_sse
134
+ @synchronizer.sync_all
135
+ @push_manager.start_sse
136
+ end
137
+ rescue StandardError => e
138
+ @config.logger.error("process_disconnect error: #{e.inspect}")
139
+ end
140
+
141
+ def record_telemetry(type, data)
142
+ @telemetry_runtime_producer.record_streaming_event(type, data)
143
+ end
144
+
145
+ def start_push_status_monitor
146
+ @config.threads[:push_status_handler] = Thread.new do
147
+ @config.logger.debug('Starting push status handler ...') if @config.debug_enabled
148
+ incoming_push_status_handler
149
+ end
150
+ end
151
+
152
+ def incoming_push_status_handler
153
+ while (status = @status_queue.pop)
154
+ @config.logger.debug("Push status handler dequeue #{status}") if @config.debug_enabled
155
+
156
+ case status
157
+ when Constants::PUSH_CONNECTED
158
+ process_connected
159
+ when Constants::PUSH_RETRYABLE_ERROR
160
+ process_disconnect(true)
161
+ when Constants::PUSH_FORCED_STOP
162
+ process_forced_stop
163
+ when Constants::PUSH_NONRETRYABLE_ERROR
164
+ process_disconnect(false)
165
+ when Constants::PUSH_SUBSYSTEM_DOWN
166
+ process_subsystem_down
167
+ when Constants::PUSH_SUBSYSTEM_READY
168
+ process_subsystem_ready
169
+ when Constants::PUSH_SUBSYSTEM_OFF
170
+ process_push_shutdown
171
+ else
172
+ @config.logger.debug('Incorrect push status type.') if @config.debug_enabled
173
+ end
174
+ end
175
+ rescue StandardError => e
176
+ @config.logger.error("Push status handler error: #{e.inspect}")
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Engine
5
+ class Synchronizer
6
+ include SplitIoClient::Cache::Fetchers
7
+ include SplitIoClient::Cache::Senders
8
+
9
+ ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES = 10
10
+
11
+ def initialize(
12
+ repositories,
13
+ config,
14
+ params
15
+ )
16
+ @splits_repository = repositories[:splits]
17
+ @segments_repository = repositories[:segments]
18
+ @rule_based_segments_repository = repositories[:rule_based_segments]
19
+ @impressions_repository = repositories[:impressions]
20
+ @events_repository = repositories[:events]
21
+ @config = config
22
+ @split_fetcher = params[:split_fetcher]
23
+ @segment_fetcher = params[:segment_fetcher]
24
+ @impressions_api = params[:impressions_api]
25
+ @impression_counter = params[:imp_counter]
26
+ @telemetry_synchronizer = params[:telemetry_synchronizer]
27
+ @impressions_sender_adapter = params[:impressions_sender_adapter]
28
+ @unique_keys_tracker = params[:unique_keys_tracker]
29
+
30
+ @splits_sync_backoff = Engine::BackOff.new(10, 0, 60)
31
+ @segments_sync_backoff = Engine::BackOff.new(10, 0, 60)
32
+ end
33
+
34
+ def sync_all(asynchronous = true)
35
+ unless asynchronous
36
+ return sync_splits_and_segments
37
+ end
38
+
39
+ @config.threads[:sync_all_thread] = Thread.new do
40
+ sync_splits_and_segments
41
+ end
42
+
43
+ true
44
+ end
45
+
46
+ def start_periodic_data_recording
47
+ unless @config.consumer?
48
+ impressions_sender
49
+ events_sender
50
+ start_telemetry_sync_task
51
+ end
52
+
53
+ impressions_count_sender
54
+ start_unique_keys_tracker_task
55
+ end
56
+
57
+ def start_periodic_fetch
58
+ @split_fetcher.call
59
+ @segment_fetcher.call
60
+ end
61
+
62
+ def stop_periodic_fetch
63
+ @split_fetcher.stop_splits_thread
64
+ @segment_fetcher.stop_segments_thread
65
+ end
66
+
67
+ def fetch_splits(target_change_number, rbs_target_change_number)
68
+ return if check_exit_conditions(target_change_number, rbs_target_change_number)
69
+
70
+ fetch_options = { cache_control_headers: true, till: nil }
71
+
72
+ result = attempt_splits_sync(target_change_number, rbs_target_change_number,
73
+ fetch_options,
74
+ @config.on_demand_fetch_max_retries,
75
+ @config.on_demand_fetch_retry_delay_seconds,
76
+ false)
77
+
78
+ attempts = @config.on_demand_fetch_max_retries - result[:remaining_attempts]
79
+ if result[:success]
80
+ @segment_fetcher.fetch_segments_if_not_exists(result[:segment_names], true) unless result[:segment_names].empty?
81
+ @config.logger.debug("Refresh completed in #{attempts} attempts.") if @config.debug_enabled
82
+
83
+ return
84
+ end
85
+
86
+ if target_change_number != 0
87
+ fetch_options[:till] = target_change_number
88
+ else
89
+ fetch_options[:till] = rbs_target_change_number
90
+ end
91
+
92
+ result = attempt_splits_sync(target_change_number, rbs_target_change_number,
93
+ fetch_options,
94
+ ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES,
95
+ nil,
96
+ true)
97
+
98
+ attempts = ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES - result[:remaining_attempts]
99
+
100
+ process_result(result, attempts)
101
+ rescue StandardError => e
102
+ @config.log_found_exception(__method__.to_s, e)
103
+ end
104
+
105
+ def fetch_segment(name, target_change_number)
106
+ return if target_change_number <= @segments_repository.get_change_number(name).to_i
107
+
108
+ fetch_options = { cache_control_headers: true, till: nil }
109
+ result = attempt_segment_sync(name,
110
+ target_change_number,
111
+ fetch_options,
112
+ @config.on_demand_fetch_max_retries,
113
+ @config.on_demand_fetch_retry_delay_seconds,
114
+ false)
115
+
116
+ attempts = @config.on_demand_fetch_max_retries - result[:remaining_attempts]
117
+ if result[:success]
118
+ @config.logger.debug("Segment #{name} refresh completed in #{attempts} attempts.") if @config.debug_enabled
119
+
120
+ return
121
+ end
122
+
123
+ fetch_options = { cache_control_headers: true, till: target_change_number }
124
+ result = attempt_segment_sync(name,
125
+ target_change_number,
126
+ fetch_options,
127
+ ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES,
128
+ nil,
129
+ true)
130
+
131
+ attempts = @config.on_demand_fetch_max_retries - result[:remaining_attempts]
132
+ if result[:success]
133
+ @config.logger.debug("Segment #{name} refresh completed bypassing the CDN in #{attempts} attempts.") if @config.debug_enabled
134
+ else
135
+ @config.logger.debug("No changes fetched for segment #{name} after #{attempts} attempts with CDN bypassed.") if @config.debug_enabled
136
+ end
137
+ rescue StandardError => e
138
+ @config.log_found_exception(__method__.to_s, e)
139
+ end
140
+
141
+ private
142
+
143
+ def process_result(result, attempts)
144
+ if result[:success]
145
+ @segment_fetcher.fetch_segments_if_not_exists(result[:segment_names], true) unless result[:segment_names].empty?
146
+ @config.logger.debug("Refresh completed bypassing the CDN in #{attempts} attempts.") if @config.debug_enabled
147
+ else
148
+ @config.logger.debug("No changes fetched after #{attempts} attempts with CDN bypassed.") if @config.debug_enabled
149
+ end
150
+ end
151
+
152
+ def attempt_segment_sync(name, target_cn, fetch_options, max_retries, retry_delay_seconds, with_backoff)
153
+ remaining_attempts = max_retries
154
+ @segments_sync_backoff.reset
155
+
156
+ loop do
157
+ remaining_attempts -= 1
158
+
159
+ @segment_fetcher.fetch_segment(name, fetch_options)
160
+
161
+ return sync_result(true, remaining_attempts) if target_cn <= @segments_repository.get_change_number(name).to_i
162
+ return sync_result(false, remaining_attempts) if remaining_attempts <= 0
163
+
164
+ delay = with_backoff ? @segments_sync_backoff.interval : retry_delay_seconds
165
+ sleep(delay)
166
+ end
167
+ end
168
+
169
+ def attempt_splits_sync(target_cn, rbs_target_cn, fetch_options, max_retries, retry_delay_seconds, with_backoff)
170
+ remaining_attempts = max_retries
171
+ @splits_sync_backoff.reset
172
+
173
+ loop do
174
+ remaining_attempts -= 1
175
+
176
+ result = @split_fetcher.fetch_splits(fetch_options)
177
+
178
+ return sync_result(true, remaining_attempts, result[:segment_names]) if check_exit_conditions(target_cn, rbs_target_cn)
179
+ return sync_result(false, remaining_attempts, result[:segment_names]) if remaining_attempts <= 0
180
+
181
+ delay = with_backoff ? @splits_sync_backoff.interval : retry_delay_seconds
182
+ sleep(delay)
183
+ end
184
+ end
185
+
186
+ # Starts thread which loops constantly and sends impressions to the Split API
187
+ def impressions_sender
188
+ ImpressionsSender.new(@impressions_repository, @config, @impressions_api).call
189
+ end
190
+
191
+ # Starts thread which loops constantly and sends events to the Split API
192
+ def events_sender
193
+ EventsSender.new(@events_repository, @config).call
194
+ end
195
+
196
+ # Starts thread which loops constantly and sends impressions count to the Split API
197
+ def impressions_count_sender
198
+ ImpressionsCountSender.new(@config, @impression_counter, @impressions_sender_adapter).call
199
+ end
200
+
201
+ def start_telemetry_sync_task
202
+ Telemetry::SyncTask.new(@config, @telemetry_synchronizer).call
203
+ end
204
+
205
+ def start_unique_keys_tracker_task
206
+ @unique_keys_tracker.call
207
+ end
208
+
209
+ def sync_result(success, remaining_attempts, segment_names = nil)
210
+ { success: success, remaining_attempts: remaining_attempts, segment_names: segment_names }
211
+ end
212
+
213
+ def sync_splits_and_segments
214
+ @config.logger.debug('Synchronizing feature flags and segments ...') if @config.debug_enabled
215
+ splits_result = @split_fetcher.fetch_splits
216
+
217
+ splits_result[:success] && @segment_fetcher.fetch_segments
218
+ end
219
+
220
+ def check_exit_conditions(target_change_number, rbs_target_change_number)
221
+ return true if rbs_target_change_number == 0 and target_change_number == 0
222
+
223
+ return target_change_number <= @splits_repository.get_change_number.to_i if rbs_target_change_number == 0
224
+
225
+ return rbs_target_change_number <= @rule_based_segments_repository.get_change_number.to_i if target_change_number == 0
226
+
227
+ return (target_change_number <= @splits_repository.get_change_number.to_i and rbs_target_change_number <= @rule_based_segments_repository.get_change_number.to_i)
228
+ end
229
+ end
230
+ end
231
+ end
@@ -1,7 +1,26 @@
1
1
  module SplitIoClient
2
2
  class SplitIoError < StandardError; end
3
3
 
4
- class SDKShutdownException < SplitIoError; end
4
+ class SDKShutdownException < Exception; end
5
5
 
6
6
  class SDKBlockerTimeoutExpiredException < SplitIoError; end
7
+
8
+ class SSEClientException < SplitIoError
9
+ attr_reader :event
10
+
11
+ def initialize(event)
12
+ @event = event
13
+ end
14
+ end
15
+
16
+ class ApiException < SplitIoError
17
+ def initialize(msg, exception_code)
18
+ @@exception_code = exception_code
19
+ super(msg)
20
+ end
21
+ def exception_code
22
+ @@exception_code
23
+ end
24
+ end
25
+
7
26
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ NO_COMPRESSION = 0
5
+ GZIP_COMPRESSION = 1
6
+ ZLIB_COMPRESSION = 2
7
+
8
+ module Helpers
9
+ class DecryptionHelper
10
+ def self.get_encoded_definition(compression, data)
11
+ case compression
12
+ when NO_COMPRESSION
13
+ Base64.decode64(data)
14
+ when GZIP_COMPRESSION
15
+ gz = Zlib::GzipReader.new(StringIO.new(Base64.decode64(data)))
16
+ gz.read
17
+ when ZLIB_COMPRESSION
18
+ Zlib::Inflate.inflate(Base64.decode64(data))
19
+ else
20
+ raise StandardError, 'Compression flag value is incorrect'
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Helpers
5
+ class EvaluatorHelper
6
+ def self.matcher_type(condition, segments_repository, rb_segment_repository)
7
+ matchers = []
8
+ segments_repository.adapter.pipelined do
9
+ condition.matchers.each do |matcher|
10
+ matchers << if matcher[:negate]
11
+ condition.negation_matcher(matcher_instance(matcher[:matcherType], condition,
12
+ matcher, segments_repository,
13
+ rb_segment_repository))
14
+ else
15
+ matcher_instance(matcher[:matcherType], condition, matcher, segments_repository, rb_segment_repository)
16
+ end
17
+ end
18
+ end
19
+ final_matcher = condition.create_condition_matcher(matchers)
20
+
21
+ if final_matcher.nil?
22
+ config.logger.error('Invalid matcher type')
23
+ else
24
+ final_matcher
25
+ end
26
+ final_matcher
27
+ end
28
+
29
+ def self.matcher_instance(type, condition, matcher, segments_repository, rb_segment_repository)
30
+ condition.send(
31
+ "matcher_#{type.downcase}",
32
+ matcher: matcher, segments_repository: segments_repository, rule_based_segments_repository: rb_segment_repository
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Helpers
5
+ class RepositoryHelper
6
+ def self.update_feature_flag_repository(feature_flag_repository, feature_flags, change_number, config, clear_storage)
7
+ to_add = []
8
+ to_delete = []
9
+ feature_flags.each do |feature_flag|
10
+ if Engine::Models::Split.archived?(feature_flag) || !feature_flag_repository.flag_set_filter.intersect?(feature_flag[:sets])
11
+ config.logger.debug("removing feature flag from store(#{feature_flag})") if config.debug_enabled
12
+ to_delete.push(feature_flag)
13
+ next
14
+ end
15
+
16
+ feature_flag = check_missing_elements(feature_flag, config)
17
+
18
+ config.logger.debug("storing feature flag (#{feature_flag[:name]})") if config.debug_enabled
19
+ to_add.push(feature_flag)
20
+ end
21
+ feature_flag_repository.clear if clear_storage
22
+ feature_flag_repository.update(to_add, to_delete, change_number)
23
+ end
24
+
25
+ def self.check_missing_elements(feature_flag, config)
26
+ unless feature_flag.key?(:impressionsDisabled)
27
+ feature_flag[:impressionsDisabled] = false
28
+ if config.debug_enabled
29
+ config.logger.debug("feature flag (#{feature_flag[:name]}) does not have impressionsDisabled field, setting it to false")
30
+ end
31
+ end
32
+
33
+ unless feature_flag.key?(:prerequisites)
34
+ feature_flag[:prerequisites] = []
35
+ if config.debug_enabled
36
+ config.logger.debug("feature flag (#{feature_flag[:name]}) does not have prerequisites field, setting it to empty array")
37
+ end
38
+ end
39
+
40
+ feature_flag
41
+ end
42
+
43
+ def self.update_rule_based_segment_repository(rule_based_segment_repository, rule_based_segments, change_number, config)
44
+ to_add = []
45
+ to_delete = []
46
+ rule_based_segments.each do |rule_based_segment|
47
+ if Engine::Models::Split.archived?(rule_based_segment)
48
+ config.logger.debug("removing rule based segment from store(#{rule_based_segment})") if config.debug_enabled
49
+ to_delete.push(rule_based_segment)
50
+ next
51
+ end
52
+
53
+ config.logger.debug("storing rule based segment (#{rule_based_segment[:name]})") if config.debug_enabled
54
+ to_add.push(rule_based_segment)
55
+ end
56
+
57
+ rule_based_segment_repository.update(to_add, to_delete, change_number)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module Helpers
5
+ class ThreadHelper
6
+ def self.stop(thread_sym, config)
7
+ thread = config.threads[thread_sym]
8
+
9
+ unless thread.nil?
10
+ config.logger.debug("Stopping #{thread_sym} thread...") if config.debug_enabled
11
+ Thread.kill(thread)
12
+ end
13
+ rescue StandardError => e
14
+ config.logger.error(e.inspect)
15
+ end
16
+
17
+ def self.alive?(thread_sym, config)
18
+ thread = config.threads[thread_sym]
19
+
20
+ thread.nil? ? false : thread.alive?
21
+ end
22
+ end
23
+ end
24
+ end