splitclient-rb 7.2.0 → 7.2.3.pre.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5a8bad0b7b72fba188bff72b10b5a0849bdadabdde484929b83071a51cfe715
4
- data.tar.gz: 27b74c33b6fbe6c87141bf163f1dbd8d37eaa586d8c60eb73319cbcb7e3cc85e
3
+ metadata.gz: f35241d91acb957b2e666ca2598021b66f8214d4d12980484b063723576dc13c
4
+ data.tar.gz: 1a833d613162de2aa18a3c925de083ddc2013bfe85630d57b2e9483a8364c357
5
5
  SHA512:
6
- metadata.gz: 0db37fb36974d3b177b3b4b08d35199bf828b8c4ebc508731e1255c44c08f9be2953174e6dbb3b1a2d514f880845b0abefd300061ad9b77e72745a867b930dc4
7
- data.tar.gz: 0d66fc9a82f117ed7ccc4e391aa59827ed024d34d3082472dab54d6c3cacd57357c01af6eb6becb69f8a0dabc12cc51258e0e7a852fbfc9c82d414eb1ad1d40b
6
+ metadata.gz: 231f86643247585e51cbb136790d71011764645edb87383c5641730b4e36952c1013dde5c06fbb46002c05b1fa07579a0b8579a3a4e859cc42862d0d2849316c
7
+ data.tar.gz: faff2962445b587f133d703ce19fec646a8495fbef7f34d8dd27d38614643bd517acf99d859b613220a4144c4f56be61c1ae048b526ef8e63a92134dfdcbda79
@@ -10,6 +10,9 @@ Metrics/MethodLength:
10
10
  Metrics/ClassLength:
11
11
  Max: 150
12
12
 
13
+ Metrics/CyclomaticComplexity:
14
+ Max: 8
15
+
13
16
  Metrics/LineLength:
14
17
  Max: 130
15
18
  Exclude:
@@ -1,5 +1,12 @@
1
1
  CHANGES
2
2
 
3
+ 7.2.2 (Dec 18, 2020)
4
+ - Fixed issue: undefined local variable or method post_impressions_count
5
+
6
+ 7.2.1 (Oct 23, 2020)
7
+ - Updated redis dependency to >= 4.2.2.
8
+ - Updated ably error handling.
9
+
3
10
  7.2.0 (Sep 25, 2020)
4
11
  - Added deduplication logic for impressions data.
5
12
  - Now there are two modes for Impressions when the SDK is in standalone mode, OPTIMIZED (default) that only ships unique impressions and DEBUG for times where you need to send ALL impressions to debug an integration.
@@ -101,6 +101,7 @@ require 'splitclient-rb/redis_metrics_fixer'
101
101
  # SSE
102
102
  require 'splitclient-rb/sse/event_source/back_off'
103
103
  require 'splitclient-rb/sse/event_source/client'
104
+ require 'splitclient-rb/sse/event_source/event_parser'
104
105
  require 'splitclient-rb/sse/event_source/event_types'
105
106
  require 'splitclient-rb/sse/event_source/stream_data'
106
107
  require 'splitclient-rb/sse/workers/segments_worker'
@@ -142,7 +142,7 @@ module SplitIoClient
142
142
 
143
143
  # General
144
144
  def exists?(key)
145
- @redis.exists(key)
145
+ @redis.exists?(key)
146
146
  end
147
147
 
148
148
  def delete(key)
@@ -40,32 +40,32 @@ module SplitIoClient
40
40
  @config.logger.info('Posting impressions count due to shutdown')
41
41
  end
42
42
  end
43
+ end
43
44
 
44
- def post_impressions_count
45
- @impressions_api.post_count(formatter(@impression_counter.pop_all))
46
- rescue StandardError => error
47
- @config.log_found_exception(__method__.to_s, error)
48
- end
49
-
50
- def formatter(counts)
51
- return if counts.empty?
45
+ def post_impressions_count
46
+ @impressions_api.post_count(formatter(@impression_counter.pop_all))
47
+ rescue StandardError => error
48
+ @config.log_found_exception(__method__.to_s, error)
49
+ end
52
50
 
53
- formated_counts = {pf: []}
51
+ def formatter(counts)
52
+ return if counts.empty?
54
53
 
55
- counts.each do |key, value|
56
- key_splited = key.split('::')
57
-
58
- formated_counts[:pf] << {
59
- f: key_splited[0].to_s, # feature name
60
- m: key_splited[1].to_i, # time frame
61
- rc: value # count
62
- }
63
- end
54
+ formated_counts = {pf: []}
64
55
 
65
- formated_counts
66
- rescue StandardError => error
67
- @config.log_found_exception(__method__.to_s, error)
56
+ counts.each do |key, value|
57
+ key_splited = key.split('::')
58
+
59
+ formated_counts[:pf] << {
60
+ f: key_splited[0].to_s, # feature name
61
+ m: key_splited[1].to_i, # time frame
62
+ rc: value # count
63
+ }
68
64
  end
65
+
66
+ formated_counts
67
+ rescue StandardError => error
68
+ @config.log_found_exception(__method__.to_s, error)
69
69
  end
70
70
  end
71
71
  end
@@ -6,5 +6,11 @@ class SplitIoClient::Constants
6
6
  CONTROL_SEC = 'control_sec'
7
7
  OCCUPANCY_CHANNEL_PREFIX = '[?occupancy=metrics.publishers]'
8
8
  FETCH_BACK_OFF_BASE_RETRIES = 1
9
+ PUSH_CONNECTED = 'PUSH_CONNECTED'
10
+ PUSH_RETRYABLE_ERROR = 'PUSH_RETRYABLE_ERROR'
11
+ PUSH_NONRETRYABLE_ERROR = 'PUSH_NONRETRYABLE_ERROR'
12
+ PUSH_SUBSYSTEM_DOWN = 'PUSH_SUBSYSTEM_DOWN'
13
+ PUSH_SUBSYSTEM_READY = 'PUSH_SUBSYSTEM_READY'
14
+ PUSH_SUBSYSTEM_OFF = 'PUSH_SUBSYSTEM_OFF'
9
15
  end
10
16
 
@@ -15,15 +15,17 @@ module SplitIoClient
15
15
  response = @auth_api_client.authenticate(@api_key)
16
16
 
17
17
  @config.logger.debug("Auth service response push_enabled: #{response[:push_enabled]}") if @config.debug_enabled
18
- if response[:push_enabled]
19
- @sse_handler.start(response[:token], response[:channels])
18
+
19
+ if response[:push_enabled] && @sse_handler.start(response[:token], response[:channels])
20
20
  schedule_next_token_refresh(response[:exp])
21
21
  @back_off.reset
22
- else
23
- stop_sse
22
+ return true
24
23
  end
25
24
 
25
+ stop_sse
26
+
26
27
  schedule_next_token_refresh(@back_off.interval) if response[:retry]
28
+ false
27
29
  rescue StandardError => e
28
30
  @config.logger.error("start_sse: #{e.inspect}")
29
31
  end
@@ -31,6 +33,7 @@ module SplitIoClient
31
33
  def stop_sse
32
34
  @sse_handler.process_disconnect if @sse_handler.sse_client.nil?
33
35
  @sse_handler.stop
36
+ SplitIoClient::Helpers::ThreadHelper.stop(:schedule_next_token_refresh, @config)
34
37
  end
35
38
 
36
39
  private
@@ -41,7 +44,7 @@ module SplitIoClient
41
44
  @config.logger.debug("schedule_next_token_refresh refresh in #{time} seconds.") if @config.debug_enabled
42
45
  sleep(time)
43
46
  @config.logger.debug('schedule_next_token_refresh starting ...') if @config.debug_enabled
44
- stop_sse
47
+ @sse_handler.stop
45
48
  start_sse
46
49
  rescue StandardError => e
47
50
  @config.logger.debug("schedule_next_token_refresh error: #{e.inspect}") if @config.debug_enabled
@@ -3,22 +3,15 @@
3
3
  module SplitIoClient
4
4
  module Engine
5
5
  class SyncManager
6
- include SplitIoClient::Cache::Fetchers
7
-
8
6
  def initialize(
9
7
  repositories,
10
8
  api_key,
11
9
  config,
12
- params
10
+ synchronizer
13
11
  )
14
- split_fetcher = SplitFetcher.new(repositories[:splits], api_key, params[:metrics], config, params[:sdk_blocker])
15
- segment_fetcher = SegmentFetcher.new(repositories[:segments], api_key, params[:metrics], config, params[:sdk_blocker])
16
- sync_params = { split_fetcher: split_fetcher, segment_fetcher: segment_fetcher, imp_counter: params[:impression_counter] }
17
-
18
- @synchronizer = Synchronizer.new(repositories, api_key, config, params[:sdk_blocker], sync_params)
12
+ @synchronizer = synchronizer
19
13
  notification_manager_keeper = SplitIoClient::SSE::NotificationManagerKeeper.new(config) do |manager|
20
- manager.on_occupancy { |publisher_available| process_occupancy(publisher_available) }
21
- manager.on_push_shutdown { process_push_shutdown }
14
+ manager.on_action { |action| process_action(action) }
22
15
  end
23
16
  @sse_handler = SplitIoClient::SSE::SSEHandler.new(
24
17
  config,
@@ -27,11 +20,11 @@ module SplitIoClient
27
20
  repositories[:segments],
28
21
  notification_manager_keeper
29
22
  ) do |handler|
30
- handler.on_connected { process_connected }
31
- handler.on_disconnect { process_disconnect }
23
+ handler.on_action { |action| process_action(action) }
32
24
  end
33
25
 
34
26
  @push_manager = PushManager.new(config, @sse_handler, api_key)
27
+ @sse_connected = Concurrent::AtomicBoolean.new(false)
35
28
  @config = config
36
29
  end
37
30
 
@@ -49,10 +42,10 @@ module SplitIoClient
49
42
  # Starts tasks if stream is enabled.
50
43
  def start_stream
51
44
  @config.logger.debug('Starting push mode ...')
52
- stream_start_thread
45
+ sync_all_thread
53
46
  @synchronizer.start_periodic_data_recording
54
47
 
55
- stream_start_sse_thread
48
+ start_sse_connection_thread
56
49
  end
57
50
 
58
51
  def start_poll
@@ -64,23 +57,24 @@ module SplitIoClient
64
57
  end
65
58
 
66
59
  # Starts thread which fetch splits and segments once and trigger task to periodic data recording.
67
- def stream_start_thread
60
+ def sync_all_thread
68
61
  @config.threads[:sync_manager_start_stream] = Thread.new do
69
62
  begin
70
63
  @synchronizer.sync_all
71
64
  rescue StandardError => e
72
- @config.logger.error("stream_start_thread error : #{e.inspect}")
65
+ @config.logger.error("sync_all_thread error : #{e.inspect}")
73
66
  end
74
67
  end
75
68
  end
76
69
 
77
70
  # Starts thread which connect to sse and after that fetch splits and segments once.
78
- def stream_start_sse_thread
71
+ def start_sse_connection_thread
79
72
  @config.threads[:sync_manager_start_sse] = Thread.new do
80
73
  begin
81
- @push_manager.start_sse
74
+ connected = @push_manager.start_sse
75
+ @synchronizer.start_periodic_fetch unless connected
82
76
  rescue StandardError => e
83
- @config.logger.error("stream_start_sse_thread error : #{e.inspect}")
77
+ @config.logger.error("start_sse_connection_thread error : #{e.inspect}")
84
78
  end
85
79
  end
86
80
  end
@@ -89,33 +83,76 @@ module SplitIoClient
89
83
  PhusionPassenger.on_event(:starting_worker_process) { |forked| start_stream if forked }
90
84
  end
91
85
 
92
- def process_connected
86
+ def process_action(action)
87
+ case action
88
+ when Constants::PUSH_CONNECTED
89
+ process_connected
90
+ when Constants::PUSH_RETRYABLE_ERROR
91
+ process_disconnect(true)
92
+ when Constants::PUSH_NONRETRYABLE_ERROR
93
+ process_disconnect(false)
94
+ when Constants::PUSH_SUBSYSTEM_DOWN
95
+ process_subsystem_down
96
+ when Constants::PUSH_SUBSYSTEM_READY
97
+ process_subsystem_ready
98
+ when Constants::PUSH_SUBSYSTEM_OFF
99
+ process_push_shutdown
100
+ else
101
+ @config.logger.debug('Incorrect action type.')
102
+ end
103
+ rescue StandardError => e
104
+ @config.logger.error("process_action error: #{e.inspect}")
105
+ end
106
+
107
+ def process_subsystem_ready
93
108
  @synchronizer.stop_periodic_fetch
94
109
  @synchronizer.sync_all
95
110
  @sse_handler.start_workers
96
- rescue StandardError => e
97
- @config.logger.error("process_connected error: #{e.inspect}")
98
111
  end
99
112
 
100
- def process_disconnect
113
+ def process_subsystem_down
114
+ @sse_handler.stop_workers
115
+ @synchronizer.start_periodic_fetch
116
+ end
117
+
118
+ def process_push_shutdown
119
+ @push_manager.stop_sse
101
120
  @sse_handler.stop_workers
102
121
  @synchronizer.start_periodic_fetch
103
122
  rescue StandardError => e
104
- @config.logger.error("process_disconnect error: #{e.inspect}")
123
+ @config.logger.error("process_push_shutdown error: #{e.inspect}")
105
124
  end
106
125
 
107
- def process_occupancy(push_enable)
108
- process_disconnect unless push_enable
109
- process_connected if push_enable
126
+ def process_connected
127
+ if @sse_connected.value
128
+ @config.logger.debug('Streaming already connected.')
129
+ return
130
+ end
131
+
132
+ @sse_connected.make_true
133
+ @synchronizer.stop_periodic_fetch
134
+ @synchronizer.sync_all
135
+ @sse_handler.start_workers
110
136
  rescue StandardError => e
111
- @config.logger.error("process_occupancy error: #{e.inspect}")
137
+ @config.logger.error("process_connected error: #{e.inspect}")
112
138
  end
113
139
 
114
- def process_push_shutdown
115
- @push_manager.stop_sse
116
- process_disconnect
140
+ def process_disconnect(reconnect)
141
+ unless @sse_connected.value
142
+ @config.logger.debug('Streaming already disconnected.')
143
+ return
144
+ end
145
+
146
+ @sse_connected.make_false
147
+ @sse_handler.stop_workers
148
+ @synchronizer.start_periodic_fetch
149
+
150
+ if reconnect
151
+ @synchronizer.sync_all
152
+ @push_manager.start_sse
153
+ end
117
154
  rescue StandardError => e
118
- @config.logger.error("process_push_shutdown error: #{e.inspect}")
155
+ @config.logger.error("process_disconnect error: #{e.inspect}")
119
156
  end
120
157
  end
121
158
  end
@@ -28,9 +28,11 @@ module SplitIoClient
28
28
  end
29
29
 
30
30
  def sync_all
31
- @config.logger.debug('Synchronizing Splits and Segments ...') if @config.debug_enabled
32
- fetch_splits
33
- fetch_segments
31
+ @config.threads[:sync_all_thread] = Thread.new do
32
+ @config.logger.debug('Synchronizing Splits and Segments ...') if @config.debug_enabled
33
+ fetch_splits
34
+ fetch_segments
35
+ end
34
36
  end
35
37
 
36
38
  def start_periodic_data_recording
@@ -7,6 +7,7 @@ module SplitIoClient
7
7
  include SplitIoClient::Cache::Repositories
8
8
  include SplitIoClient::Cache::Stores
9
9
  include SplitIoClient::Cache::Senders
10
+ include SplitIoClient::Cache::Fetchers
10
11
 
11
12
  attr_reader :adapter, :client, :manager, :config
12
13
 
@@ -53,8 +54,12 @@ module SplitIoClient
53
54
  if @config.localhost_mode
54
55
  start_localhost_components
55
56
  else
56
- params = { sdk_blocker: @sdk_blocker, metrics: @metrics, impression_counter: @impression_counter }
57
- SplitIoClient::Engine::SyncManager.new(repositories, @api_key, @config, params).start
57
+ split_fetcher = SplitFetcher.new(@splits_repository, @api_key, @metrics, config, @sdk_blocker)
58
+ segment_fetcher = SegmentFetcher.new(@segments_repository, @api_key, @metrics, config, @sdk_blocker)
59
+ params = { split_fetcher: split_fetcher, segment_fetcher: segment_fetcher, imp_counter: @impression_counter }
60
+
61
+ synchronizer = SplitIoClient::Engine::Synchronizer.new(repositories, @api_key, @config, @sdk_blocker, params)
62
+ SplitIoClient::Engine::SyncManager.new(repositories, @api_key, @config, synchronizer).start
58
63
  end
59
64
  end
60
65
 
@@ -125,14 +130,13 @@ module SplitIoClient
125
130
  end
126
131
 
127
132
  def repositories
128
- repos = {}
129
- repos[:splits] = @splits_repository
130
- repos[:segments] = @segments_repository
131
- repos[:impressions] = @impressions_repository
132
- repos[:events] = @events_repository
133
- repos[:metrics] = @metrics_repository
134
-
135
- repos
133
+ {
134
+ splits: @splits_repository,
135
+ segments: @segments_repository,
136
+ impressions: @impressions_repository,
137
+ events: @events_repository,
138
+ metrics: @metrics_repository
139
+ }
136
140
  end
137
141
 
138
142
  def start_localhost_components
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: false
2
2
 
3
- require 'concurrent/atomics'
4
3
  require 'socketry'
5
4
  require 'uri'
6
5
 
@@ -9,16 +8,19 @@ module SplitIoClient
9
8
  module EventSource
10
9
  class Client
11
10
  DEFAULT_READ_TIMEOUT = 70
11
+ CONNECT_TIMEOUT = 30_000
12
+ OK_CODE = 200
12
13
  KEEP_ALIVE_RESPONSE = "c\r\n:keepalive\n\n\r\n".freeze
14
+ ERROR_EVENT_TYPE = 'error'.freeze
13
15
 
14
16
  def initialize(config, read_timeout: DEFAULT_READ_TIMEOUT)
15
17
  @config = config
16
18
  @read_timeout = read_timeout
17
19
  @connected = Concurrent::AtomicBoolean.new(false)
20
+ @first_event = Concurrent::AtomicBoolean.new(true)
18
21
  @socket = nil
19
- @back_off = BackOff.new(@config.streaming_reconnect_back_off_base)
20
-
21
- @on = { event: ->(_) {}, connected: ->(_) {}, disconnect: ->(_) {} }
22
+ @event_parser = SSE::EventSource::EventParser.new(config)
23
+ @on = { event: ->(_) {}, action: ->(_) {} }
22
24
 
23
25
  yield self if block_given?
24
26
  end
@@ -27,16 +29,12 @@ module SplitIoClient
27
29
  @on[:event] = action
28
30
  end
29
31
 
30
- def on_connected(&action)
31
- @on[:connected] = action
32
- end
33
-
34
- def on_disconnect(&action)
35
- @on[:disconnect] = action
32
+ def on_action(&action)
33
+ @on[:action] = action
36
34
  end
37
35
 
38
- def close
39
- dispatch_disconnect
36
+ def close(action = Constants::PUSH_NONRETRYABLE_ERROR)
37
+ dispatch_action(action)
40
38
  @connected.make_false
41
39
  SplitIoClient::Helpers::ThreadHelper.stop(:connect_stream, @config)
42
40
  @socket&.close
@@ -46,10 +44,16 @@ module SplitIoClient
46
44
 
47
45
  def start(url)
48
46
  @uri = URI(url)
47
+ latch = Concurrent::CountDownLatch.new(1)
48
+
49
+ connect_thread(latch)
49
50
 
50
- connect_thread
51
+ return false unless latch.wait(CONNECT_TIMEOUT)
52
+
53
+ connected?
51
54
  rescue StandardError => e
52
55
  @config.logger.error("SSEClient start Error: #{e.inspect}")
56
+ connected?
53
57
  end
54
58
 
55
59
  def connected?
@@ -58,30 +62,27 @@ module SplitIoClient
58
62
 
59
63
  private
60
64
 
61
- def connect_thread
65
+ def connect_thread(latch)
62
66
  @config.threads[:connect_stream] = Thread.new do
63
67
  @config.logger.info('Starting connect_stream thread ...') if @config.debug_enabled
64
- connect_stream
68
+ connect_stream(latch)
65
69
  end
66
70
  end
67
71
 
68
- def connect_stream
69
- interval = @back_off.interval
70
- sleep(interval) if interval.positive?
71
-
72
+ def connect_stream(latch)
72
73
  socket_write
73
74
 
74
- while @connected.value
75
+ while connected? || @first_event.value
75
76
  begin
76
77
  partial_data = @socket.readpartial(10_000, timeout: @read_timeout)
77
78
 
79
+ read_first_event(partial_data, latch)
80
+
78
81
  raise 'eof exception' if partial_data == :eof
79
82
  rescue StandardError => e
80
- @config.logger.error(e.inspect) if @config.debug_enabled
81
- @connected.make_false
82
- @socket&.close
83
- @socket = nil
84
- connect_stream
83
+ @config.logger.error('Error reading partial data: ' + e.inspect) if @config.debug_enabled
84
+ close(Constants::PUSH_RETRYABLE_ERROR)
85
+ return
85
86
  end
86
87
 
87
88
  process_data(partial_data)
@@ -89,12 +90,31 @@ module SplitIoClient
89
90
  end
90
91
 
91
92
  def socket_write
93
+ @first_event.make_true
92
94
  @socket = socket_connect
93
95
  @socket.write(build_request(@uri))
94
- dispatch_connected
95
96
  rescue StandardError => e
96
97
  @config.logger.error("Error during connecting to #{@uri.host}. Error: #{e.inspect}")
97
- close
98
+ close(Constants::PUSH_NONRETRYABLE_ERROR)
99
+ end
100
+
101
+ def read_first_event(data, latch)
102
+ return unless @first_event.value
103
+
104
+ response_code = @event_parser.first_event(data)
105
+ @config.logger.debug("SSE client first event code: #{response_code}")
106
+
107
+ error_event = false
108
+ events = @event_parser.parse(data)
109
+ events.each { |e| error_event = true if e.event_type == ERROR_EVENT_TYPE }
110
+ @first_event.make_false
111
+
112
+ if response_code == OK_CODE && !error_event
113
+ @connected.make_true
114
+ dispatch_action(Constants::PUSH_CONNECTED)
115
+ end
116
+
117
+ latch.count_down
98
118
  end
99
119
 
100
120
  def socket_connect
@@ -104,11 +124,11 @@ module SplitIoClient
104
124
  end
105
125
 
106
126
  def process_data(partial_data)
107
- unless partial_data.nil? || partial_data == KEEP_ALIVE_RESPONSE
108
- @config.logger.debug("Event partial data: #{partial_data}") if @config.debug_enabled
109
- buffer = read_partial_data(partial_data)
110
- parse_event(buffer)
111
- end
127
+ return if partial_data.nil? || partial_data == KEEP_ALIVE_RESPONSE
128
+
129
+ @config.logger.debug("Event partial data: #{partial_data}") if @config.debug_enabled
130
+ events = @event_parser.parse(partial_data)
131
+ events.each { |event| process_event(event) }
112
132
  rescue StandardError => e
113
133
  @config.logger.error("process_data error: #{e.inspect}")
114
134
  end
@@ -122,64 +142,32 @@ module SplitIoClient
122
142
  req
123
143
  end
124
144
 
125
- def read_partial_data(data)
126
- buffer = ''
127
- buffer << data
128
- buffer.chomp!
129
- buffer.split("\n")
130
- end
131
-
132
- def parse_event(buffer)
133
- type = nil
134
-
135
- buffer.each do |d|
136
- splited_data = d.split(':')
137
-
138
- case splited_data[0]
139
- when 'event'
140
- type = splited_data[1].strip
141
- when 'data'
142
- data = parse_event_data(d, type)
143
- unless type.nil? || data[:data].nil?
144
- event = StreamData.new(type, data[:client_id], data[:data], data[:channel])
145
- dispatch_event(event)
146
- end
147
- end
145
+ def process_event(event)
146
+ case event.event_type
147
+ when ERROR_EVENT_TYPE
148
+ dispatch_error(event)
149
+ else
150
+ dispatch_event(event)
148
151
  end
149
- rescue StandardError => e
150
- @config.logger.error("Error during parsing a event: #{e.inspect}")
151
152
  end
152
153
 
153
- def parse_event_data(data, type)
154
- event_data = JSON.parse(data.sub('data: ', ''))
155
- client_id = event_data['clientId']&.strip
156
- channel = event_data['channel']&.strip
157
- parsed_data = JSON.parse(event_data['data']) unless type == 'error'
158
- parsed_data = event_data if type == 'error'
159
-
160
- { client_id: client_id, channel: channel, data: parsed_data }
154
+ def dispatch_error(event)
155
+ @config.logger.error("Event error: #{event.event_type}, #{event.data}")
156
+ if event.data['code'] >= 40_140 && event.data['code'] <= 40_149
157
+ close(Constants::PUSH_RETRYABLE_ERROR)
158
+ elsif event.data['code'] >= 40_000 && event.data['code'] <= 49_999
159
+ close(Constants::PUSH_NONRETRYABLE_ERROR)
160
+ end
161
161
  end
162
162
 
163
163
  def dispatch_event(event)
164
- raise SSEClientException.new(event), 'Error event' if event.event_type == 'error'
165
-
166
164
  @config.logger.debug("Dispatching event: #{event.event_type}, #{event.channel}") if @config.debug_enabled
167
165
  @on[:event].call(event)
168
- rescue SSEClientException => e
169
- @config.logger.error("Event error: #{e.event.event_type}, #{e.event.data}")
170
- close
171
- end
172
-
173
- def dispatch_connected
174
- @connected.make_true
175
- @back_off.reset
176
- @config.logger.debug('Dispatching connected') if @config.debug_enabled
177
- @on[:connected].call
178
166
  end
179
167
 
180
- def dispatch_disconnect
181
- @config.logger.debug('Dispatching disconnect') if @config.debug_enabled
182
- @on[:disconnect].call
168
+ def dispatch_action(action)
169
+ @config.logger.debug("Dispatching action: #{action}") if @config.debug_enabled
170
+ @on[:action].call(action)
183
171
  end
184
172
  end
185
173
  end
@@ -0,0 +1,64 @@
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}")
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.debug("Error parsing first event: #{e.inspect}")
40
+ BAD_REQUEST_CODE
41
+ end
42
+
43
+ private
44
+
45
+ def parse_event_data(data, type)
46
+ event_data = JSON.parse(data.sub('data: ', ''))
47
+ client_id = event_data['clientId']&.strip
48
+ channel = event_data['channel']&.strip
49
+ parsed_data = JSON.parse(event_data['data']) unless type == 'error'
50
+ parsed_data = event_data if type == 'error'
51
+
52
+ { client_id: client_id, channel: channel, data: parsed_data }
53
+ end
54
+
55
+ def read_partial_data(data)
56
+ buffer = ''
57
+ buffer << data
58
+ buffer.chomp!
59
+ buffer.split("\n")
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent/atomics'
3
+ require 'concurrent'
4
4
 
5
5
  module SplitIoClient
6
6
  module SSE
@@ -8,7 +8,9 @@ module SplitIoClient
8
8
  def initialize(config)
9
9
  @config = config
10
10
  @publisher_available = Concurrent::AtomicBoolean.new(true)
11
- @on = { occupancy: ->(_) {}, push_shutdown: ->(_) {} }
11
+ @publishers_pri = Concurrent::AtomicFixnum.new
12
+ @publishers_sec = Concurrent::AtomicFixnum.new
13
+ @on = { action: ->(_) {} }
12
14
 
13
15
  yield self if block_given?
14
16
  end
@@ -16,19 +18,15 @@ module SplitIoClient
16
18
  def handle_incoming_occupancy_event(event)
17
19
  if event.data['type'] == 'CONTROL'
18
20
  process_event_control(event.data['controlType'])
19
- elsif event.channel == SplitIoClient::Constants::CONTROL_PRI
20
- process_event_occupancy(event.data['metrics']['publishers'])
21
+ else
22
+ process_event_occupancy(event.channel, event.data['metrics']['publishers'])
21
23
  end
22
24
  rescue StandardError => e
23
25
  @config.logger.error(e)
24
26
  end
25
27
 
26
- def on_occupancy(&action)
27
- @on[:occupancy] = action
28
- end
29
-
30
- def on_push_shutdown(&action)
31
- @on[:push_shutdown] = action
28
+ def on_action(&action)
29
+ @on[:action] = action
32
30
  end
33
31
 
34
32
  private
@@ -36,35 +34,42 @@ module SplitIoClient
36
34
  def process_event_control(type)
37
35
  case type
38
36
  when 'STREAMING_PAUSED'
39
- dispatch_occupancy_event(false)
37
+ dispatch_action(Constants::PUSH_SUBSYSTEM_DOWN)
40
38
  when 'STREAMING_RESUMED'
41
- dispatch_occupancy_event(true) if @publisher_available.value
39
+ dispatch_action(Constants::PUSH_SUBSYSTEM_READY) if @publisher_available.value
42
40
  when 'STREAMING_DISABLED'
43
- dispatch_push_shutdown
41
+ dispatch_action(Constants::PUSH_SUBSYSTEM_OFF)
44
42
  else
45
43
  @config.logger.error("Incorrect event type: #{incoming_notification}")
46
44
  end
47
45
  end
48
46
 
49
- def process_event_occupancy(publishers)
50
- @config.logger.debug("Occupancy process event with #{publishers} publishers") if @config.debug_enabled
51
- if publishers <= 0 && @publisher_available.value
47
+ def process_event_occupancy(channel, publishers)
48
+ @config.logger.debug("Processed occupancy event with #{publishers} publishers. Channel: #{channel}")
49
+
50
+ update_publishers(channel, publishers)
51
+
52
+ if !are_publishers_available? && @publisher_available.value
52
53
  @publisher_available.make_false
53
- dispatch_occupancy_event(false)
54
- elsif publishers >= 1 && !@publisher_available.value
54
+ dispatch_action(Constants::PUSH_SUBSYSTEM_DOWN)
55
+ elsif are_publishers_available? && !@publisher_available.value
55
56
  @publisher_available.make_true
56
- dispatch_occupancy_event(true)
57
+ dispatch_action(Constants::PUSH_SUBSYSTEM_READY)
57
58
  end
58
59
  end
59
60
 
60
- def dispatch_occupancy_event(push_enable)
61
- @config.logger.debug("Dispatching occupancy event with publisher avaliable: #{push_enable}")
62
- @on[:occupancy].call(push_enable)
61
+ def update_publishers(channel, publishers)
62
+ @publishers_pri.value = publishers if channel == Constants::CONTROL_PRI
63
+ @publishers_sec.value = publishers if channel == Constants::CONTROL_SEC
64
+ end
65
+
66
+ def are_publishers_available?
67
+ @publishers_pri.value.positive? || @publishers_sec.value.positive?
63
68
  end
64
69
 
65
- def dispatch_push_shutdown
66
- @config.logger.debug('Dispatching push shutdown')
67
- @on[:push_shutdown].call
70
+ def dispatch_action(action)
71
+ @config.logger.debug("Dispatching action: #{action}")
72
+ @on[:action].call(action)
68
73
  end
69
74
  end
70
75
  end
@@ -13,11 +13,10 @@ module SplitIoClient
13
13
  @notification_processor = SplitIoClient::SSE::NotificationProcessor.new(config, @splits_worker, @segments_worker)
14
14
  @sse_client = SSE::EventSource::Client.new(@config) do |client|
15
15
  client.on_event { |event| handle_incoming_message(event) }
16
- client.on_connected { process_connected }
17
- client.on_disconnect { process_disconnect }
16
+ client.on_action { |action| process_action(action) }
18
17
  end
19
18
 
20
- @on = { connected: ->(_) {}, disconnect: ->(_) {} }
19
+ @on = { action: ->(_) {} }
21
20
 
22
21
  yield self if block_given?
23
22
  end
@@ -48,22 +47,14 @@ module SplitIoClient
48
47
  @segments_worker.stop
49
48
  end
50
49
 
51
- def on_connected(&action)
52
- @on[:connected] = action
53
- end
54
-
55
- def on_disconnect(&action)
56
- @on[:disconnect] = action
57
- end
58
-
59
- def process_disconnect
60
- @on[:disconnect].call
50
+ def on_action(&action)
51
+ @on[:action] = action
61
52
  end
62
53
 
63
54
  private
64
55
 
65
- def process_connected
66
- @on[:connected].call
56
+ def process_action(action)
57
+ @on[:action].call(action)
67
58
  end
68
59
 
69
60
  def handle_incoming_message(notification)
@@ -8,27 +8,39 @@ module SplitIoClient
8
8
  @synchronizer = synchronizer
9
9
  @config = config
10
10
  @segments_repository = segments_repository
11
- @queue = nil
12
- end
13
-
14
- def start
15
- return if SplitIoClient::Helpers::ThreadHelper.alive?(:segment_update_worker, @config)
16
-
17
11
  @queue = Queue.new
18
- perform_thread
12
+ @running = Concurrent::AtomicBoolean.new(false)
19
13
  end
20
14
 
21
15
  def add_to_queue(change_number, segment_name)
22
- return if @queue.nil?
16
+ unless @running.value
17
+ @config.logger.debug('segments worker not running.')
18
+ return
19
+ end
23
20
 
24
21
  item = { change_number: change_number, segment_name: segment_name }
25
22
  @config.logger.debug("SegmentsWorker add to queue #{item}")
26
23
  @queue.push(item)
27
24
  end
28
25
 
26
+ def start
27
+ if @running.value
28
+ @config.logger.debug('segments worker already running.')
29
+ return
30
+ end
31
+
32
+ @running.make_true
33
+ perform_thread
34
+ end
35
+
29
36
  def stop
37
+ unless @running.value
38
+ @config.logger.debug('segments worker not running.')
39
+ return
40
+ end
41
+
42
+ @running.make_false
30
43
  SplitIoClient::Helpers::ThreadHelper.stop(:segment_update_worker, @config)
31
- @queue = nil
32
44
  end
33
45
 
34
46
  private
@@ -8,35 +8,53 @@ module SplitIoClient
8
8
  @synchronizer = synchronizer
9
9
  @config = config
10
10
  @splits_repository = splits_repository
11
+ @queue = Queue.new
12
+ @running = Concurrent::AtomicBoolean.new(false)
11
13
  end
12
14
 
13
15
  def start
14
- return if SplitIoClient::Helpers::ThreadHelper.alive?(:split_update_worker, @config)
16
+ if @running.value
17
+ @config.logger.debug('splits worker already running.')
18
+ return
19
+ end
15
20
 
16
- @queue = Queue.new
21
+ @running.make_true
17
22
  perform_thread
18
23
  end
19
24
 
25
+ def stop
26
+ unless @running.value
27
+ @config.logger.debug('splits worker not running.')
28
+ return
29
+ end
30
+
31
+ @running.make_false
32
+ SplitIoClient::Helpers::ThreadHelper.stop(:split_update_worker, @config)
33
+ end
34
+
20
35
  def add_to_queue(change_number)
21
- return if @queue.nil?
36
+ unless @running.value
37
+ @config.logger.debug('splits worker not running.')
38
+ return
39
+ end
22
40
 
23
41
  @config.logger.debug("SplitsWorker add to queue #{change_number}")
24
42
  @queue.push(change_number)
25
43
  end
26
44
 
27
45
  def kill_split(change_number, split_name, default_treatment)
28
- return if @queue.nil?
46
+ unless @running.value
47
+ @config.logger.debug('splits worker not running.')
48
+ return
49
+ end
50
+
51
+ return if @splits_repository.get_change_number.to_i > change_number
29
52
 
30
53
  @config.logger.debug("SplitsWorker kill #{split_name}, #{change_number}")
31
54
  @splits_repository.kill(change_number, split_name, default_treatment)
32
55
  add_to_queue(change_number)
33
56
  end
34
57
 
35
- def stop
36
- SplitIoClient::Helpers::ThreadHelper.stop(:split_update_worker, @config)
37
- @queue = nil
38
- end
39
-
40
58
  private
41
59
 
42
60
  def perform
@@ -1,3 +1,3 @@
1
1
  module SplitIoClient
2
- VERSION = '7.2.0'
2
+ VERSION = '7.2.3.pre.rc1'
3
3
  end
@@ -55,7 +55,7 @@ Gem::Specification.new do |spec|
55
55
  spec.add_runtime_dependency 'jwt', '>= 2.2.1'
56
56
  spec.add_runtime_dependency 'lru_redux'
57
57
  spec.add_runtime_dependency 'net-http-persistent', '>= 2.9'
58
- spec.add_runtime_dependency 'redis', '>= 3.2'
58
+ spec.add_runtime_dependency 'redis', '>= 4.2.2'
59
59
  spec.add_runtime_dependency 'socketry', '~> 0.5.1'
60
60
  spec.add_runtime_dependency 'thread_safe', '>= 0.3'
61
61
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: splitclient-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.0
4
+ version: 7.2.3.pre.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Split Software
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-28 00:00:00.000000000 Z
11
+ date: 2021-01-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: allocation_stats
@@ -268,14 +268,14 @@ dependencies:
268
268
  requirements:
269
269
  - - ">="
270
270
  - !ruby/object:Gem::Version
271
- version: '3.2'
271
+ version: 4.2.2
272
272
  type: :runtime
273
273
  prerelease: false
274
274
  version_requirements: !ruby/object:Gem::Requirement
275
275
  requirements:
276
276
  - - ">="
277
277
  - !ruby/object:Gem::Version
278
- version: '3.2'
278
+ version: 4.2.2
279
279
  - !ruby/object:Gem::Dependency
280
280
  name: socketry
281
281
  requirement: !ruby/object:Gem::Requirement
@@ -425,6 +425,7 @@ files:
425
425
  - lib/splitclient-rb/split_logger.rb
426
426
  - lib/splitclient-rb/sse/event_source/back_off.rb
427
427
  - lib/splitclient-rb/sse/event_source/client.rb
428
+ - lib/splitclient-rb/sse/event_source/event_parser.rb
428
429
  - lib/splitclient-rb/sse/event_source/event_types.rb
429
430
  - lib/splitclient-rb/sse/event_source/stream_data.rb
430
431
  - lib/splitclient-rb/sse/notification_manager_keeper.rb
@@ -454,9 +455,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
454
455
  version: '0'
455
456
  required_rubygems_version: !ruby/object:Gem::Requirement
456
457
  requirements:
457
- - - ">="
458
+ - - ">"
458
459
  - !ruby/object:Gem::Version
459
- version: '0'
460
+ version: 1.3.1
460
461
  requirements: []
461
462
  rubygems_version: 3.0.6
462
463
  signing_key: