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,263 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'socket'
4
+ require 'openssl'
5
+ require 'uri'
6
+ require 'timeout'
7
+
8
+ module SplitIoClient
9
+ module SSE
10
+ module EventSource
11
+ class Client
12
+ DEFAULT_READ_TIMEOUT = 70
13
+ CONNECT_TIMEOUT = 30_000
14
+ OK_CODE = 200
15
+ KEEP_ALIVE_RESPONSE = "c\r\n:keepalive\n\n\r\n".freeze
16
+ ERROR_EVENT_TYPE = 'error'.freeze
17
+
18
+ def initialize(config,
19
+ api_key,
20
+ telemetry_runtime_producer,
21
+ event_parser,
22
+ notification_manager_keeper,
23
+ notification_processor,
24
+ status_queue,
25
+ read_timeout: DEFAULT_READ_TIMEOUT)
26
+ @config = config
27
+ @api_key = api_key
28
+ @telemetry_runtime_producer = telemetry_runtime_producer
29
+ @event_parser = event_parser
30
+ @notification_manager_keeper = notification_manager_keeper
31
+ @notification_processor = notification_processor
32
+ @status_queue = status_queue
33
+ @read_timeout = read_timeout
34
+ @connected = Concurrent::AtomicBoolean.new(false)
35
+ @first_event = Concurrent::AtomicBoolean.new(true)
36
+ @socket = nil
37
+ end
38
+
39
+ def close(status = nil)
40
+ unless connected?
41
+ @config.logger.debug('SSEClient already disconected.') if @config.debug_enabled
42
+ return
43
+ end
44
+ @config.logger.debug("Closing SSEClient socket") if @config.debug_enabled
45
+
46
+ push_status(status)
47
+ @connected.make_false
48
+ @socket.sync_close = true if @socket.is_a? OpenSSL::SSL::SSLSocket
49
+ @socket.close
50
+ @config.logger.debug("SSEClient socket state #{@socket.state}") if @socket.is_a? OpenSSL::SSL::SSLSocket && @config.debug_enabled
51
+ rescue StandardError => e
52
+ @config.logger.error("SSEClient close Error: #{e.inspect}")
53
+ end
54
+
55
+ def start(url)
56
+ if connected?
57
+ @config.logger.debug('SSEClient already running.') if @config.debug_enabled
58
+ return true
59
+ end
60
+
61
+ @uri = URI(url)
62
+ latch = Concurrent::CountDownLatch.new(1)
63
+ connect_thread(latch)
64
+
65
+ return false unless latch.wait(CONNECT_TIMEOUT)
66
+
67
+ connected?
68
+ rescue StandardError => e
69
+ @config.logger.error("SSEClient start Error: #{e.inspect}")
70
+ connected?
71
+ end
72
+
73
+ def connected?
74
+ @connected.value
75
+ end
76
+
77
+ private
78
+
79
+ def connect_thread(latch)
80
+ @config.threads[:connect_stream] = Thread.new do
81
+ @config.logger.info('Starting connect_stream thread ...')
82
+ new_status = connect_stream(latch)
83
+ push_status(new_status)
84
+ @config.logger.info('connect_stream thread finished.')
85
+ end
86
+ end
87
+
88
+ def connect_stream(latch)
89
+ return Constants::PUSH_NONRETRYABLE_ERROR unless socket_write(latch)
90
+ while connected? || @first_event.value
91
+ begin
92
+ if IO.select([@socket], nil, nil, @read_timeout)
93
+ begin
94
+ partial_data = @socket.readpartial(10_000)
95
+ read_first_event(partial_data, latch)
96
+
97
+ raise 'eof exception' if partial_data == :eof
98
+ rescue IO::WaitReadable => e
99
+ @config.logger.debug("SSE client IO::WaitReadable transient error: #{e.inspect}") if @config.debug_enabled
100
+ IO.select([@socket], nil, nil, @read_timeout)
101
+ retry
102
+ rescue Errno::EAGAIN => e
103
+ @config.logger.debug("SSE client transient error: #{e.inspect}") if @config.debug_enabled
104
+ IO.select([@socket], nil, nil, @read_timeout)
105
+ retry
106
+ rescue Errno::ETIMEDOUT => e
107
+ @config.logger.error("SSE read operation timed out!: #{e.inspect}")
108
+ return Constants::PUSH_RETRYABLE_ERROR
109
+ rescue EOFError => e
110
+ @config.logger.error("SSE read operation EOF Exception!: #{e.inspect}")
111
+ raise 'eof exception'
112
+ rescue Errno::EBADF, IOError => e
113
+ @config.logger.error("SSE read operation EBADF or IOError: #{e.inspect}")
114
+ return Constants::PUSH_RETRYABLE_ERROR
115
+ rescue StandardError => e
116
+ @config.logger.error("SSE read operation StandardError: #{e.inspect}")
117
+ return nil if ENV['SPLITCLIENT_ENV'] == 'test'
118
+
119
+ @config.logger.error("Error reading partial data: #{e.inspect}")
120
+ return Constants::PUSH_RETRYABLE_ERROR
121
+ end
122
+ else
123
+ @config.logger.error("SSE read operation timed out, no data available.")
124
+ return Constants::PUSH_RETRYABLE_ERROR
125
+ end
126
+ rescue Errno::EBADF
127
+ @config.logger.debug("SSE socket is not connected (Errno::EBADF)") if @config.debug_enabled
128
+ break
129
+ rescue RuntimeError
130
+ raise 'eof exception'
131
+ rescue Exception => e
132
+ @config.logger.debug("SSE socket is not connected: #{e.inspect}") if @config.debug_enabled
133
+ break
134
+ end
135
+
136
+ process_data(partial_data)
137
+ end
138
+ @config.logger.info("SSE read operation exited: #{connected?}")
139
+
140
+ nil
141
+ end
142
+
143
+ def socket_write(latch)
144
+ @first_event.make_true
145
+ @socket = socket_connect
146
+ @socket.puts(build_request(@uri))
147
+ true
148
+ rescue StandardError => e
149
+ @config.logger.error("Error during connecting to #{@uri.host}. Error: #{e.inspect}")
150
+ latch.count_down
151
+ false
152
+ end
153
+
154
+ def read_first_event(data, latch)
155
+ return unless @first_event.value
156
+
157
+ response_code = @event_parser.first_event(data)
158
+ @config.logger.debug("SSE client first event code: #{response_code}") if @config.debug_enabled
159
+
160
+ error_event = false
161
+ events = @event_parser.parse(data)
162
+ events.each { |e| error_event = true if e.event_type == ERROR_EVENT_TYPE }
163
+ @first_event.make_false
164
+
165
+ if response_code == OK_CODE && !error_event
166
+ @connected.make_true
167
+ @config.logger.debug("SSE client first event Connected is true") if @config.debug_enabled
168
+ @telemetry_runtime_producer.record_streaming_event(Telemetry::Domain::Constants::SSE_CONNECTION_ESTABLISHED, nil)
169
+ push_status(Constants::PUSH_CONNECTED)
170
+ end
171
+
172
+ latch.count_down
173
+ end
174
+
175
+ def socket_connect
176
+ tcp_socket = TCPSocket.new(@uri.host, @uri.port)
177
+ if @uri.scheme.casecmp('https').zero?
178
+ begin
179
+ ssl_context = OpenSSL::SSL::SSLContext.new
180
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
181
+ ssl_socket.hostname = @uri.host
182
+
183
+ begin
184
+ ssl_socket.connect_nonblock
185
+ rescue IO::WaitReadable
186
+ IO.select([ssl_socket])
187
+ retry
188
+ rescue IO::WaitWritable
189
+ IO.select(nil, [ssl_socket])
190
+ retry
191
+ end
192
+ return ssl_socket
193
+
194
+ rescue Exception => e
195
+ @config.logger.error("socket connect error: #{e.inspect}")
196
+ return nil
197
+ end
198
+ end
199
+
200
+ tcp_socket
201
+ end
202
+
203
+ def process_data(partial_data)
204
+ @config.logger.debug("Event partial data: #{partial_data}") if @config.debug_enabled
205
+ return if partial_data.nil? || partial_data == KEEP_ALIVE_RESPONSE
206
+
207
+ events = @event_parser.parse(partial_data)
208
+ events.each { |event| process_event(event) }
209
+ rescue StandardError => e
210
+ @config.logger.error("process_data error: #{e.inspect}")
211
+ end
212
+
213
+ def build_request(uri)
214
+ req = "GET #{uri.request_uri} HTTP/1.1\r\n"
215
+ req << "Host: #{uri.host}\r\n"
216
+ req << "Accept: text/event-stream\r\n"
217
+ req << "SplitSDKVersion: #{@config.language}-#{@config.version}\r\n"
218
+ req << "SplitSDKMachineIP: #{@config.machine_ip}\r\n"
219
+ req << "SplitSDKMachineName: #{@config.machine_name}\r\n"
220
+ req << "SplitSDKClientKey: #{@api_key.split(//).last(4).join}\r\n" unless @api_key.nil?
221
+ req << "Cache-Control: no-cache\r\n\r\n"
222
+ @config.logger.debug("Request info: #{req}") if @config.debug_enabled
223
+ req
224
+ end
225
+
226
+ def process_event(event)
227
+ case event.event_type
228
+ when ERROR_EVENT_TYPE
229
+ dispatch_error(event)
230
+ else
231
+ dispatch_event(event)
232
+ end
233
+ end
234
+
235
+ def dispatch_error(event)
236
+ @config.logger.error("Event error: #{event.event_type}, #{event.data}")
237
+ @telemetry_runtime_producer.record_streaming_event(Telemetry::Domain::Constants::ABLY_ERROR, event.data['code'])
238
+
239
+ if event.data['code'] >= 40_140 && event.data['code'] <= 40_149
240
+ close(Constants::PUSH_RETRYABLE_ERROR)
241
+ elsif event.data['code'] >= 40_000 && event.data['code'] <= 49_999
242
+ close(Constants::PUSH_NONRETRYABLE_ERROR)
243
+ end
244
+ end
245
+
246
+ def dispatch_event(event)
247
+ if event.occupancy?
248
+ @notification_manager_keeper.handle_incoming_occupancy_event(event)
249
+ else
250
+ @notification_processor.process(event)
251
+ end
252
+ end
253
+
254
+ def push_status(status)
255
+ return if status.nil?
256
+
257
+ @config.logger.debug("Pushing new sse status: #{status}") if @config.debug_enabled
258
+ @status_queue.push(status)
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: false
2
+
3
+ module SplitIoClient
4
+ module SSE
5
+ module EventSource
6
+ class EventParser
7
+ BAD_REQUEST_CODE = 400
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ end
12
+
13
+ def parse(raw_event)
14
+ type = nil
15
+ events = []
16
+ buffer = read_partial_data(raw_event)
17
+
18
+ buffer.each do |d|
19
+ splited_data = d.split(':')
20
+
21
+ case splited_data[0]
22
+ when 'event'
23
+ type = splited_data[1].strip
24
+ when 'data'
25
+ data = parse_event_data(d, type)
26
+ events << StreamData.new(type, data[:client_id], data[:data], data[:channel]) unless type.nil? || data[:data].nil?
27
+ end
28
+ end
29
+
30
+ events
31
+ rescue StandardError => e
32
+ @config.logger.debug("Error during parsing a event: #{e.inspect}") if @config.debug_enabled
33
+ []
34
+ end
35
+
36
+ def first_event(raw_data)
37
+ raw_data.split("\n")[0].split(' ')[1].to_i
38
+ rescue StandardError => e
39
+ @config.logger.error("Error parsing first event: #{e.inspect}")
40
+ BAD_REQUEST_CODE
41
+ end
42
+
43
+ private
44
+
45
+ def parse_event_data(data, type)
46
+ data_value = data.sub('data:', '')
47
+ event_data = JSON.parse(data_value.strip)
48
+ client_id = event_data['clientId']&.strip
49
+ channel = event_data['channel']&.strip
50
+ parsed_data = JSON.parse(event_data['data']) unless type == 'error'
51
+ parsed_data = event_data if type == 'error'
52
+
53
+ { client_id: client_id, channel: channel, data: parsed_data }
54
+ end
55
+
56
+ def read_partial_data(data)
57
+ buffer = ''
58
+ buffer << data
59
+ buffer.chomp!
60
+ buffer.split("\n")
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module SSE
5
+ module EventSource
6
+ class EventTypes
7
+ SPLIT_UPDATE = 'SPLIT_UPDATE'
8
+ SPLIT_KILL = 'SPLIT_KILL'
9
+ SEGMENT_UPDATE = 'SEGMENT_UPDATE'
10
+ CONTROL = 'CONTROL'
11
+ RB_SEGMENT_UPDATE = 'RB_SEGMENT_UPDATE'
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: false
2
+
3
+ module SplitIoClient
4
+ module SSE
5
+ module EventSource
6
+ class StreamData
7
+ attr_reader :event_type, :channel, :data, :client_id
8
+
9
+ def initialize(event_type, client_id, data, channel)
10
+ @event_type = event_type
11
+ @client_id = client_id
12
+ @data = data
13
+ @channel = channel&.gsub(SplitIoClient::Constants::OCCUPANCY_CHANNEL_PREFIX, '')
14
+ end
15
+
16
+ def occupancy?
17
+ @channel.include? 'control'
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent'
4
+
5
+ module SplitIoClient
6
+ module SSE
7
+ class NotificationManagerKeeper
8
+ DISABLED = 0
9
+ ENABLED = 1
10
+ PAUSED = 2
11
+
12
+ def initialize(config, telemetry_runtime_producer, status_queue)
13
+ @config = config
14
+ @telemetry_runtime_producer = telemetry_runtime_producer
15
+ @status_queue = status_queue
16
+ @publisher_available = Concurrent::AtomicBoolean.new(true)
17
+ @publishers_pri = Concurrent::AtomicFixnum.new(2)
18
+ @publishers_sec = Concurrent::AtomicFixnum.new(2)
19
+ end
20
+
21
+ def handle_incoming_occupancy_event(event)
22
+ if event.data['type'] == 'CONTROL'
23
+ process_event_control(event.data['controlType'])
24
+ else
25
+ process_event_occupancy(event.channel, event.data['metrics']['publishers'])
26
+ end
27
+ rescue StandardError => e
28
+ @config.logger.error(e)
29
+ end
30
+
31
+ private
32
+
33
+ def process_event_control(type)
34
+ case type
35
+ when 'STREAMING_PAUSED'
36
+ @telemetry_runtime_producer.record_streaming_event(Telemetry::Domain::Constants::STREAMING_STATUS, PAUSED)
37
+ push_status(Constants::PUSH_SUBSYSTEM_DOWN)
38
+ when 'STREAMING_RESUMED'
39
+ @telemetry_runtime_producer.record_streaming_event(Telemetry::Domain::Constants::STREAMING_STATUS, ENABLED)
40
+ push_status(Constants::PUSH_SUBSYSTEM_READY) if @publisher_available.value
41
+ when 'STREAMING_DISABLED'
42
+ @telemetry_runtime_producer.record_streaming_event(Telemetry::Domain::Constants::STREAMING_STATUS, DISABLED)
43
+ push_status(Constants::PUSH_SUBSYSTEM_OFF)
44
+ else
45
+ @config.logger.error("Incorrect event type: #{incoming_notification}") if @config.debug_enabled
46
+ end
47
+ end
48
+
49
+ def process_event_occupancy(channel, publishers)
50
+ @config.logger.debug("Processed occupancy event with #{publishers} publishers. Channel: #{channel}") if @config.debug_enabled
51
+
52
+ update_publishers(channel, publishers)
53
+
54
+ if !are_publishers_available? && @publisher_available.value
55
+ @publisher_available.make_false
56
+ push_status(Constants::PUSH_SUBSYSTEM_DOWN)
57
+ elsif are_publishers_available? && !@publisher_available.value
58
+ @publisher_available.make_true
59
+ push_status(Constants::PUSH_SUBSYSTEM_READY)
60
+ end
61
+ end
62
+
63
+ def update_publishers(channel, publishers)
64
+ case channel
65
+ when Constants::CONTROL_PRI
66
+ @telemetry_runtime_producer.record_streaming_event(Telemetry::Domain::Constants::OCCUPANCY_PRI, publishers)
67
+ @publishers_pri.value = publishers
68
+ when Constants::CONTROL_SEC
69
+ @telemetry_runtime_producer.record_streaming_event(Telemetry::Domain::Constants::OCCUPANCY_SEC, publishers)
70
+ @publishers_sec.value = publishers
71
+ end
72
+ end
73
+
74
+ def are_publishers_available?
75
+ @publishers_pri.value.positive? || @publishers_sec.value.positive?
76
+ end
77
+
78
+ def push_status(status)
79
+ @config.logger.debug("Pushing occupancy status: #{status}") if @config.debug_enabled
80
+ @status_queue.push(status)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module SSE
5
+ class NotificationProcessor
6
+ def initialize(config, splits_worker, segments_worker)
7
+ @config = config
8
+ @splits_worker = splits_worker
9
+ @segments_worker = segments_worker
10
+ end
11
+
12
+ def process(incoming_notification)
13
+ case incoming_notification.data['type']
14
+ when SSE::EventSource::EventTypes::SPLIT_UPDATE
15
+ process_split_update(incoming_notification)
16
+ when SSE::EventSource::EventTypes::RB_SEGMENT_UPDATE
17
+ process_split_update(incoming_notification)
18
+ when SSE::EventSource::EventTypes::SPLIT_KILL
19
+ process_split_kill(incoming_notification)
20
+ when SSE::EventSource::EventTypes::SEGMENT_UPDATE
21
+ process_segment_update(incoming_notification)
22
+ else
23
+ @config.logger.error("Incorrect event type: #{incoming_notification}")
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def process_split_update(notification)
30
+ @config.logger.debug("#{notification.event_type} notification received: #{notification}") if @config.debug_enabled
31
+ @splits_worker.add_to_queue(notification)
32
+ end
33
+
34
+ def process_split_kill(notification)
35
+ @config.logger.debug("SPLIT KILL notification received: #{notification}") if @config.debug_enabled
36
+ @splits_worker.add_to_queue(notification)
37
+ end
38
+
39
+ def process_segment_update(notification)
40
+ @config.logger.debug("SEGMENT UPDATE notification received: #{notification}") if @config.debug_enabled
41
+ change_number = notification.data['changeNumber']
42
+ segment_name = notification.data['segmentName']
43
+
44
+ @segments_worker.add_to_queue(change_number, segment_name)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module SSE
5
+ class SSEHandler
6
+ attr_reader :sse_client
7
+
8
+ def initialize(config,
9
+ splits_worker,
10
+ segments_worker,
11
+ sse_client)
12
+ @config = config
13
+ @splits_worker = splits_worker
14
+ @segments_worker = segments_worker
15
+ @sse_client = sse_client
16
+ end
17
+
18
+ def start(token_jwt, channels)
19
+ @sse_client.start("#{@config.streaming_service_url}?channels=#{channels}&v=1.1&accessToken=#{token_jwt}")
20
+ end
21
+
22
+ def stop
23
+ @sse_client.close(Constants::PUSH_FORCED_STOP)
24
+ stop_workers
25
+ rescue StandardError => e
26
+ @config.logger.debug("SSEHandler stop error: #{e.inspect}") if @config.debug_enabled
27
+ end
28
+
29
+ def connected?
30
+ @sse_client&.connected? || false
31
+ end
32
+
33
+ def start_workers
34
+ @splits_worker.start
35
+ @segments_worker.start
36
+ end
37
+
38
+ def stop_workers
39
+ @splits_worker.stop
40
+ @segments_worker.stop
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SplitIoClient
4
+ module SSE
5
+ module Workers
6
+ class SegmentsWorker
7
+ def initialize(synchronizer, config, segments_repository)
8
+ @synchronizer = synchronizer
9
+ @config = config
10
+ @segments_repository = segments_repository
11
+ @queue = Queue.new
12
+ @running = Concurrent::AtomicBoolean.new(false)
13
+ end
14
+
15
+ def add_to_queue(change_number, segment_name)
16
+ item = { change_number: change_number, segment_name: segment_name }
17
+ @config.logger.debug("SegmentsWorker add to queue #{item}") if @config.debug_enabled
18
+ @queue.push(item)
19
+ end
20
+
21
+ def start
22
+ if @running.value
23
+ @config.logger.debug('segments worker already running.') if @config.debug_enabled
24
+ return
25
+ end
26
+
27
+ @running.make_true
28
+ perform_thread
29
+ end
30
+
31
+ def stop
32
+ unless @running.value
33
+ @config.logger.debug('segments worker not running.') if @config.debug_enabled
34
+ return
35
+ end
36
+
37
+ @running.make_false
38
+ SplitIoClient::Helpers::ThreadHelper.stop(:segment_update_worker, @config)
39
+ end
40
+
41
+ private
42
+
43
+ def perform
44
+ while (item = @queue.pop)
45
+ segment_name = item[:segment_name]
46
+ cn = item[:change_number]
47
+ @config.logger.debug("SegmentsWorker change_number dequeue #{segment_name}, #{cn}") if @config.debug_enabled
48
+
49
+ @synchronizer.fetch_segment(segment_name, cn)
50
+ end
51
+ end
52
+
53
+ def perform_thread
54
+ @config.threads[:segment_update_worker] = Thread.new do
55
+ @config.logger.debug('Starting segments worker ...') if @config.debug_enabled
56
+ perform
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end