splitclient-rb 8.11.0 → 8.11.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41d269b5dc5e38906001aefe42cb60bb4ef8de5376b9d71c613b5c8345cc9451
4
- data.tar.gz: 6d66df400c0e2279ac23250dc22540135bd4e358467b5ba17ade47f8efdc6be9
3
+ metadata.gz: f75f1490dbebf79ecb7799bb96a29f180ed5259036edbd4351b3db9f1f2e1cba
4
+ data.tar.gz: e8f83fe01180e3a4a7487c82fd2a2c58cafdc971b951b9eb0c0c472351b00c4d
5
5
  SHA512:
6
- metadata.gz: 1fd4c042221d85b902894870073ec8dda740807f59348c83ce470e5571d929b6e4ca88d19e014c597a91841464a89b4f3c558b35993f091b2d5bc142dc0bfb0e
7
- data.tar.gz: b1bb37265463a9a8023f70cce31dbc880a2a84c0618b3defc4e0bef9075bb62cc558f68f212e9ea1b4310be51d180a1f5538333b6b30a2e079ce704fd4da638d
6
+ metadata.gz: 7d8a983e3f53437d2749ac0a7f60b83e2d124001f396b19389ee19cb63d2cee404ef4253835586414a793fbb737c803869db51f2114b0931ea7f474aa9bdfc1d
7
+ data.tar.gz: b6cc38e9b8d9d54b6d7858f02780059dc9b26dab95e9c022c5f92311621fc0057c4cddc1a1c2ea07b9831a27799ec5665cf5ef4b6cbc43dc0e27b4264179f51c
data/CHANGES.txt CHANGED
@@ -1,5 +1,15 @@
1
1
  CHANGES
2
2
 
3
+ 8.11.1 (Jul, 23, 2026)
4
+ - Added a retry mechanism in case the initial SSE socket connection failed, and extended the list of retryable errors when socket exceptions are caught during streaming.
5
+ - Fixed debug logging in rule-based segment matcher class when debug_enabled parameter is not set in configuration .
6
+ - Fixed a debug log that caused exception when streaming socket closes.
7
+
8
+ 8.11.0 (Mar, 12, 2026)
9
+ - Added the ability to listen to different events triggered by the SDK. Read more in our docs.
10
+ - SDK_UPDATE notify when a flag or user segment has changed
11
+ - SDK_READY notify when the SDK is ready to evaluate
12
+
3
13
  8.10.1 (Jan 28, 2025)
4
14
  - Fixed rule-based segment matcher to exit when a conition is met.
5
15
  - Fixed impressions properties format in redis mode.
data/LICENSE CHANGED
@@ -157,7 +157,7 @@ Apache License
157
157
  file or class name and description of purpose be included on the
158
158
  same "printed page" as the copyright notice for easier
159
159
  identification within third-party archives.
160
- Copyright [yyyy] [name of copyright owner]
160
+ Copyright 2025 Harness Corporation
161
161
  Licensed under the Apache License, Version 2.0 (the "License");
162
162
  you may not use this file except in compliance with the License.
163
163
  You may obtain a copy of the License at
@@ -23,7 +23,6 @@ module SplitIoClient
23
23
  name = segment[:name]
24
24
 
25
25
  @adapter.initialize_set(segment_data(name)) unless @adapter.exists?(segment_data(name))
26
- @config.logger.debug(segment)
27
26
  add_keys(name, segment[:added])
28
27
  remove_keys(name, segment[:removed])
29
28
  if segment[:added].length > 0 || segment[:removed].length > 0
@@ -21,7 +21,9 @@ module SplitIoClient
21
21
  return process_error(response) if response.status >= 400 && response.status < 500
22
22
 
23
23
  @telemetry_runtime_producer.record_sync_error(Telemetry::Domain::Constants::TOKEN_SYNC, response.status.to_i)
24
- @config.logger.debug("Error connecting to: #{@config.auth_service_url}. Response status: #{response.status}") if @config.debug_enabled
24
+ if @config.debug_enabled
25
+ @config.logger.debug("Error connecting to: #{@config.auth_service_url}. Response status: #{response.status}")
26
+ end
25
27
  { push_enabled: false, retry: true }
26
28
  rescue StandardError => e
27
29
  @config.logger.debug("AuthApiClient error: #{e.inspect}.") if @config.debug_enabled
@@ -51,7 +53,9 @@ module SplitIoClient
51
53
  end
52
54
 
53
55
  def process_error(response)
54
- @config.logger.debug("Error connecting to: #{@config.auth_service_url}. Response status: #{response.status}") if @config.debug_enabled
56
+ if @config.debug_enabled
57
+ @config.logger.debug("Error connecting to: #{@config.auth_service_url}. Response status: #{response.status}")
58
+ end
55
59
  @telemetry_runtime_producer.record_auth_rejections if response.status == 401
56
60
 
57
61
  { push_enabled: false, retry: false }
@@ -18,15 +18,15 @@ module SplitIoClient
18
18
 
19
19
  @mutex.synchronize do
20
20
  # SDK ready already fired
21
- if sdk_event == SplitIoClient::Engine::Models::SdkEvent::SDK_READY && event_already_triggered(sdk_event)
22
- @active_subscriptions[sdk_event] = SplitIoClient::Engine::Models::EventActiveSubscriptions.new(true, event_handler)
21
+ if sdk_event == Engine::Models::SdkEvent::SDK_READY && event_already_triggered(sdk_event)
22
+ @active_subscriptions[sdk_event] = Engine::Models::EventActiveSubscriptions.new(true, event_handler)
23
23
  @config.logger.debug('EventsManager: Firing SDK_READY event for new subscription') if @config.debug_enabled
24
24
  fire_sdk_event(sdk_event, nil)
25
25
  return
26
26
  end
27
27
 
28
28
  @config.logger.debug("EventsManager: Register event: #{sdk_event}") if @config.debug_enabled
29
- @active_subscriptions[sdk_event] = SplitIoClient::Engine::Models::EventActiveSubscriptions.new(false, event_handler)
29
+ @active_subscriptions[sdk_event] = Engine::Models::EventActiveSubscriptions.new(false, event_handler)
30
30
  end
31
31
  end
32
32
 
@@ -66,7 +66,7 @@ module SplitIoClient
66
66
  private
67
67
 
68
68
  def check_if_register_needed(sorted_event)
69
- sorted_event == SplitIoClient::Engine::Models::SdkEvent::SDK_READY &&
69
+ sorted_event == Engine::Models::SdkEvent::SDK_READY &&
70
70
  get_event_handler(sorted_event).nil? &&
71
71
  !@active_subscriptions.include?(sorted_event)
72
72
  end
@@ -110,7 +110,7 @@ module SplitIoClient
110
110
  end
111
111
 
112
112
  def get_sdk_event_if_applicable(sdk_internal_event)
113
- final_sdk_event = SplitIoClient::Engine::Models::ValidSdkEvent.new(nil, false)
113
+ final_sdk_event = Engine::Models::ValidSdkEvent.new(nil, false)
114
114
 
115
115
  events_to_fire = []
116
116
  require_any_sdk_event = check_require_any(sdk_internal_event)
@@ -118,7 +118,7 @@ module SplitIoClient
118
118
  if (!event_already_triggered(require_any_sdk_event.sdk_event) &&
119
119
  execution_limit(require_any_sdk_event.sdk_event) == 1) ||
120
120
  execution_limit(require_any_sdk_event.sdk_event) == -1
121
- final_sdk_event = SplitIoClient::Engine::Models::ValidSdkEvent.new(
121
+ final_sdk_event = Engine::Models::ValidSdkEvent.new(
122
122
  require_any_sdk_event.sdk_event,
123
123
  check_prerequisites(require_any_sdk_event.sdk_event) &&
124
124
  check_suppressed_by(require_any_sdk_event.sdk_event)
@@ -178,10 +178,10 @@ module SplitIoClient
178
178
  end
179
179
 
180
180
  def check_require_any(sdk_internal_event)
181
- valid_sdk_event = SplitIoClient::Engine::Models::ValidSdkEvent.new(nil, false)
181
+ valid_sdk_event = Engine::Models::ValidSdkEvent.new(nil, false)
182
182
  @manager_config.require_any.each do |name, val|
183
183
  if val.include?(sdk_internal_event)
184
- valid_sdk_event = SplitIoClient::Engine::Models::ValidSdkEvent.new(name, true)
184
+ valid_sdk_event = Engine::Models::ValidSdkEvent.new(name, true)
185
185
  return valid_sdk_event
186
186
  end
187
187
  end
@@ -40,7 +40,7 @@ module SplitIoClient
40
40
  break if matched
41
41
  end
42
42
 
43
- @logger.debug("[InRuleSegmentMatcher] #{@segment_name} is in rule based segment -> #{matched}")
43
+ @config.logger.debug("[InRuleSegmentMatcher] #{@segment_name} is in rule based segment -> #{matched}") if @config.debug_enabled
44
44
  matched
45
45
  end
46
46
 
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: false
2
2
 
3
3
  module SplitIoClient
4
- module Engine::Models
5
- class SdkInternalEventNotification
6
- attr_reader :internal_event, :metadata
4
+ module Engine
5
+ module Models
6
+ class SdkInternalEventNotification
7
+ attr_reader :internal_event, :metadata
7
8
 
8
- def initialize(internal_event, metadata)
9
- @internal_event = internal_event
10
- @metadata = metadata
9
+ def initialize(internal_event, metadata)
10
+ @internal_event = internal_event
11
+ @metadata = metadata
12
+ end
11
13
  end
12
14
  end
13
15
  end
@@ -16,17 +16,21 @@ module SplitIoClient
16
16
  response = @auth_api_client.authenticate(@api_key)
17
17
  @config.logger.debug("Auth service response push_enabled: #{response[:push_enabled]}") if @config.debug_enabled
18
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])
19
+ unless response[:push_enabled]
20
+ schedule_next_token_refresh(@back_off.interval) if response[:retry]
21
+ return false
22
+ end
23
23
 
24
- return true
24
+ unless @sse_handler.start(response[:token], response[:channels])
25
+ @config.logger.debug('Streaming server returned error') if @config.debug_enabled
26
+ stop_sse
27
+ return false
25
28
  end
26
29
 
27
- stop_sse
28
- schedule_next_token_refresh(@back_off.interval) if response[:retry]
29
- false
30
+ schedule_next_token_refresh(response[:exp])
31
+ @back_off.reset
32
+ record_telemetry(response[:exp])
33
+ true
30
34
  rescue StandardError => e
31
35
  @config.logger.error("start_sse: #{e.inspect}")
32
36
  end
@@ -6,14 +6,8 @@ module SplitIoClient
6
6
  SYNC_MODE_STREAMING = 0
7
7
  SYNC_MODE_POLLING = 1
8
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)
9
+ def initialize(config, synchronizer, telemetry_runtime_producer, telemetry_synchronizer,
10
+ status_manager, sse_handler, push_manager, status_queue)
17
11
  @config = config
18
12
  @synchronizer = synchronizer
19
13
  @telemetry_runtime_producer = telemetry_runtime_producer
@@ -23,6 +17,7 @@ module SplitIoClient
23
17
  @push_manager = push_manager
24
18
  @status_queue = status_queue
25
19
  @sse_connected = Concurrent::AtomicBoolean.new(false)
20
+ @back_off = Engine::BackOff.new(1, 3)
26
21
  end
27
22
 
28
23
  def start
@@ -119,7 +114,7 @@ module SplitIoClient
119
114
  end
120
115
 
121
116
  def process_disconnect(reconnect)
122
- unless @sse_connected.value
117
+ unless @sse_connected.value || reconnect
123
118
  @config.logger.debug('Streaming already disconnected.') if @config.debug_enabled
124
119
  return
125
120
  end
@@ -130,6 +125,9 @@ module SplitIoClient
130
125
  record_telemetry(Telemetry::Domain::Constants::SYNC_MODE, SYNC_MODE_POLLING)
131
126
 
132
127
  if reconnect
128
+ wait_interval = @back_off.interval
129
+ @config.logger.debug("Retrying streaming connection in: #{wait_interval} seconds")
130
+ sleep(wait_interval)
133
131
  @push_manager.stop_sse
134
132
  @synchronizer.sync_all
135
133
  @push_manager.start_sse
@@ -155,6 +153,7 @@ module SplitIoClient
155
153
 
156
154
  case status
157
155
  when Constants::PUSH_CONNECTED
156
+ @back_off.reset
158
157
  process_connected
159
158
  when Constants::PUSH_RETRYABLE_ERROR
160
159
  process_disconnect(true)
@@ -169,12 +168,16 @@ module SplitIoClient
169
168
  when Constants::PUSH_SUBSYSTEM_OFF
170
169
  process_push_shutdown
171
170
  else
172
- @config.logger.debug('Incorrect push status type.') if @config.debug_enabled
171
+ log_if_debug('Incorrect push status type.')
173
172
  end
174
173
  end
175
174
  rescue StandardError => e
176
175
  @config.logger.error("Push status handler error: #{e.inspect}")
177
176
  end
178
177
  end
178
+
179
+ def log_if_debug(msg)
180
+ @config.logger.debug(msg) if @config.debug_enabled
181
+ end
179
182
  end
180
183
  end
@@ -55,8 +55,8 @@ module SplitIoClient
55
55
  end
56
56
 
57
57
  def start_periodic_fetch
58
- @split_fetcher.call
59
- @segment_fetcher.call
58
+ @split_fetcher.call unless Helpers::ThreadHelper.alive?(:split_fetcher, @config)
59
+ @segment_fetcher.call unless Helpers::ThreadHelper.alive?(:segment_fetcher, @config)
60
60
  end
61
61
 
62
62
  def stop_periodic_fetch
@@ -10,7 +10,7 @@ module SplitIoClient
10
10
  module EventSource
11
11
  class Client
12
12
  DEFAULT_READ_TIMEOUT = 70
13
- CONNECT_TIMEOUT = 30_000
13
+ CONNECT_TIMEOUT = 30
14
14
  OK_CODE = 200
15
15
  KEEP_ALIVE_RESPONSE = "c\r\n:keepalive\n\n\r\n".freeze
16
16
  ERROR_EVENT_TYPE = 'error'.freeze
@@ -37,17 +37,15 @@ module SplitIoClient
37
37
  end
38
38
 
39
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
40
+ return if @socket.nil?
45
41
 
42
+ @config.logger.debug("Closing SSEClient socket") if @config.debug_enabled
46
43
  push_status(status)
47
44
  @connected.make_false
48
45
  @socket.sync_close = true if @socket.is_a? OpenSSL::SSL::SSLSocket
49
46
  @socket.close
50
- @config.logger.debug("SSEClient socket state #{@socket.state}") if @socket.is_a? OpenSSL::SSL::SSLSocket && @config.debug_enabled
47
+ @config.logger.debug("SSEClient socket state #{@socket.state}") if @socket.is_a?(OpenSSL::SSL::SSLSocket) && @config.debug_enabled
48
+ @socket = nil
51
49
  rescue StandardError => e
52
50
  @config.logger.error("SSEClient close Error: #{e.inspect}")
53
51
  end
@@ -80,21 +78,21 @@ module SplitIoClient
80
78
  @config.threads[:connect_stream] = Thread.new do
81
79
  @config.logger.info('Starting connect_stream thread ...')
82
80
  new_status = connect_stream(latch)
83
- push_status(new_status)
81
+ push_status(new_status) unless new_status.nil?
84
82
  @config.logger.info('connect_stream thread finished.')
85
83
  end
86
84
  end
87
85
 
88
86
  def connect_stream(latch)
89
- return Constants::PUSH_NONRETRYABLE_ERROR unless socket_write(latch)
87
+ return Constants::PUSH_RETRYABLE_ERROR unless socket_write(latch)
90
88
  while connected? || @first_event.value
91
89
  begin
92
90
  if IO.select([@socket], nil, nil, @read_timeout)
93
91
  begin
94
92
  partial_data = @socket.readpartial(10_000)
95
- read_first_event(partial_data, latch)
96
93
 
97
- raise 'eof exception' if partial_data == :eof
94
+ first_event_status = read_first_event(partial_data, latch)
95
+ return first_event_status unless first_event_status.nil?
98
96
  rescue IO::WaitReadable => e
99
97
  @config.logger.debug("SSE client IO::WaitReadable transient error: #{e.inspect}") if @config.debug_enabled
100
98
  IO.select([@socket], nil, nil, @read_timeout)
@@ -107,8 +105,8 @@ module SplitIoClient
107
105
  @config.logger.error("SSE read operation timed out!: #{e.inspect}")
108
106
  return Constants::PUSH_RETRYABLE_ERROR
109
107
  rescue EOFError => e
110
- @config.logger.error("SSE read operation EOF Exception!: #{e.inspect}")
111
- raise 'eof exception'
108
+ @config.logger.error("SSE read operation EOF, server closed the connection, will reconnect: #{e.inspect}")
109
+ return Constants::PUSH_RETRYABLE_ERROR
112
110
  rescue Errno::EBADF, IOError => e
113
111
  @config.logger.error("SSE read operation EBADF or IOError: #{e.inspect}")
114
112
  return Constants::PUSH_RETRYABLE_ERROR
@@ -123,14 +121,9 @@ module SplitIoClient
123
121
  @config.logger.error("SSE read operation timed out, no data available.")
124
122
  return Constants::PUSH_RETRYABLE_ERROR
125
123
  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
124
  rescue Exception => e
132
125
  @config.logger.debug("SSE socket is not connected: #{e.inspect}") if @config.debug_enabled
133
- break
126
+ return Constants::PUSH_RETRYABLE_ERROR
134
127
  end
135
128
 
136
129
  process_data(partial_data)
@@ -157,19 +150,20 @@ module SplitIoClient
157
150
  response_code = @event_parser.first_event(data)
158
151
  @config.logger.debug("SSE client first event code: #{response_code}") if @config.debug_enabled
159
152
 
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
153
  @first_event.make_false
164
154
 
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)
155
+ if response_code != OK_CODE
156
+ @config.logger.error("SSE first event failed, code: #{response_code}")
157
+ latch.count_down
158
+ return Constants::PUSH_RETRYABLE_ERROR
170
159
  end
171
160
 
161
+ @connected.make_true
162
+ @config.logger.debug("SSE client first event Connected is true") if @config.debug_enabled
163
+ @telemetry_runtime_producer.record_streaming_event(Telemetry::Domain::Constants::SSE_CONNECTION_ESTABLISHED, nil)
164
+ push_status(Constants::PUSH_CONNECTED)
172
165
  latch.count_down
166
+ return nil
173
167
  end
174
168
 
175
169
  def socket_connect
@@ -52,7 +52,9 @@ module SplitIoClient
52
52
 
53
53
  def perform
54
54
  while (notification = @queue.pop)
55
- @config.logger.debug("feature_flags_worker change_number dequeue #{notification.data['changeNumber']}") if @config.debug_enabled
55
+ if @config.debug_enabled
56
+ @config.logger.debug("feature_flags_worker change_number dequeue #{notification.data['changeNumber']}")
57
+ end
56
58
  case notification.data['type']
57
59
  when SSE::EventSource::EventTypes::SPLIT_UPDATE
58
60
  success = update_feature_flag(notification)
@@ -117,7 +119,9 @@ module SplitIoClient
117
119
  def kill_feature_flag(notification)
118
120
  return if @feature_flags_repository.get_change_number.to_i > notification.data['changeNumber']
119
121
 
120
- @config.logger.debug("feature_flags_worker kill #{notification.data['splitName']}, #{notification.data['changeNumber']}") if @config.debug_enabled
122
+ if @config.debug_enabled
123
+ @config.logger.debug("feature_flags_worker kill #{notification.data['splitName']}, #{notification.data['changeNumber']}")
124
+ end
121
125
  @feature_flags_repository.kill(notification.data['changeNumber'],
122
126
  notification.data['splitName'],
123
127
  notification.data['defaultTreatment'])
@@ -1,3 +1,3 @@
1
1
  module SplitIoClient
2
- VERSION = '8.11.0'
2
+ VERSION = '8.11.1'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: splitclient-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.11.0
4
+ version: 8.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Split Software