From c9f5d0bcd216086a7344b5d73c54555151351857 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 21 Jul 2026 08:59:15 -0700 Subject: [PATCH 01/15] updated eof: event to none retryable error --- lib/splitclient-rb/sse/event_source/client.rb | 14 +++++------ lib/splitclient-rb/version.rb | 2 +- spec/sse/event_source/client_spec.rb | 23 ++++++++++++++----- spec/sse/sse_handler_spec.rb | 3 +-- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/lib/splitclient-rb/sse/event_source/client.rb b/lib/splitclient-rb/sse/event_source/client.rb index 74c0cfc3..933ebb07 100644 --- a/lib/splitclient-rb/sse/event_source/client.rb +++ b/lib/splitclient-rb/sse/event_source/client.rb @@ -37,17 +37,15 @@ def initialize(config, end def close(status = nil) - unless connected? - @config.logger.debug('SSEClient already disconected.') if @config.debug_enabled - return - end - @config.logger.debug("Closing SSEClient socket") if @config.debug_enabled + return if @socket.nil? + @config.logger.debug("Closing SSEClient socket") if @config.debug_enabled push_status(status) @connected.make_false @socket.sync_close = true if @socket.is_a? OpenSSL::SSL::SSLSocket @socket.close @config.logger.debug("SSEClient socket state #{@socket.state}") if @socket.is_a?(OpenSSL::SSL::SSLSocket) && @config.debug_enabled + @socket = nil rescue StandardError => e @config.logger.error("SSEClient close Error: #{e.inspect}") end @@ -95,8 +93,8 @@ def connect_stream(latch) read_first_event(partial_data, latch) if partial_data == :eof - @config.logger.error("SSE recived EOF unexpectedly") - return Constants::PUSH_RETRYABLE_ERROR + @config.logger.error("SSE recived EOF unexpectedly") if @config.debug_enabled + return Constants::PUSH_NONRETRYABLE_ERROR end rescue IO::WaitReadable => e @config.logger.debug("SSE client IO::WaitReadable transient error: #{e.inspect}") if @config.debug_enabled @@ -111,7 +109,7 @@ def connect_stream(latch) return Constants::PUSH_RETRYABLE_ERROR rescue EOFError => e @config.logger.error("SSE read operation EOF Exception!: #{e.inspect}") - return Constants::PUSH_RETRYABLE_ERROR + return Constants::PUSH_NONRETRYABLE_ERROR rescue Errno::EBADF, IOError => e @config.logger.error("SSE read operation EBADF or IOError: #{e.inspect}") return Constants::PUSH_RETRYABLE_ERROR diff --git a/lib/splitclient-rb/version.rb b/lib/splitclient-rb/version.rb index 24237148..49692951 100644 --- a/lib/splitclient-rb/version.rb +++ b/lib/splitclient-rb/version.rb @@ -1,3 +1,3 @@ module SplitIoClient - VERSION = '8.11.1-beta1' + VERSION = '8.11.1-rc2' end diff --git a/spec/sse/event_source/client_spec.rb b/spec/sse/event_source/client_spec.rb index ca14cc47..8753c6dc 100644 --- a/spec/sse/event_source/client_spec.rb +++ b/spec/sse/event_source/client_spec.rb @@ -217,7 +217,13 @@ expect(connected).to eq(false) expect(sse_client.connected?).to eq(false) - expect { push_status_queue.pop(true) }.to raise_error(ThreadError) + + latch = Concurrent::CountDownLatch.new(1) + + thr2 = Thread.new do + res = sse_client.send(:connect_stream, latch) + expect(res).to eq(SplitIoClient::Constants::PUSH_NONRETRYABLE_ERROR) + end stop_workers end @@ -261,9 +267,13 @@ start_workers sse_client = subject.new(config, api_token, telemetry_runtime_producer, event_parser, notification_manager_keeper, notification_processor, push_status_queue) - connected = sse_client.start(server.base_uri) - expect(connected).to eq(false) - expect { push_status_queue.pop(true) }.to raise_error(ThreadError) + sse_client.instance_variable_set(:@uri, URI(server.base_uri)) + latch = Concurrent::CountDownLatch.new(1) + + thr2 = Thread.new do + res = sse_client.send(:connect_stream, latch) + expect(res).to eq(SplitIoClient::Constants::PUSH_NONRETRYABLE_ERROR) + end stop_workers end @@ -315,11 +325,12 @@ allow(sse_client).to receive(:read_first_event).and_raise(EOFError) sleep(1) + thr1 = Thread.new do - sse_client.send(:connect_stream, latch) + res = sse_client.send(:connect_stream, latch) + expect(res).to eq(SplitIoClient::Constants::PUSH_NONRETRYABLE_ERROR) end sleep(1) - allow(sse_client).to receive(:read_first_event).and_return(true) expect(log.string).to include 'SSE read operation EOF Exception' stop_workers diff --git a/spec/sse/sse_handler_spec.rb b/spec/sse/sse_handler_spec.rb index 1c81abcf..9a6e5368 100644 --- a/spec/sse/sse_handler_spec.rb +++ b/spec/sse/sse_handler_spec.rb @@ -74,10 +74,9 @@ expect(connected).to eq(false) expect(sse_handler.connected?).to eq(false) expect(sse_handler.sse_client.connected?).to eq(false) - expect { push_status_queue.pop(true) }.to raise_error(ThreadError) + expect(push_status_queue.pop(true)).to eq(SplitIoClient::Constants::PUSH_RETRYABLE_ERROR) sse_handler.stop - expect { push_status_queue.pop(true) }.to raise_error(ThreadError) end end From 199d66a12312c1901d2569350dc9e82f1ab4ca46 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 21 Jul 2026 09:02:57 -0700 Subject: [PATCH 02/15] polish --- lib/splitclient-rb/sse/event_source/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/splitclient-rb/sse/event_source/client.rb b/lib/splitclient-rb/sse/event_source/client.rb index 933ebb07..198c7c66 100644 --- a/lib/splitclient-rb/sse/event_source/client.rb +++ b/lib/splitclient-rb/sse/event_source/client.rb @@ -93,7 +93,7 @@ def connect_stream(latch) read_first_event(partial_data, latch) if partial_data == :eof - @config.logger.error("SSE recived EOF unexpectedly") if @config.debug_enabled + @config.logger.error("SSE recived EOF unexpectedly, disconnecting") return Constants::PUSH_NONRETRYABLE_ERROR end rescue IO::WaitReadable => e From c67a7be017f2d492f5e2c1f2419f030994ad21e4 Mon Sep 17 00:00:00 2001 From: Mauro Antonio Sanz Date: Tue, 21 Jul 2026 14:19:36 -0300 Subject: [PATCH 03/15] suggestions --- lib/splitclient-rb/sse/event_source/client.rb | 9 ++------- spec/sse/event_source/client_spec.rb | 6 +++--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/splitclient-rb/sse/event_source/client.rb b/lib/splitclient-rb/sse/event_source/client.rb index 198c7c66..d2e0ed16 100644 --- a/lib/splitclient-rb/sse/event_source/client.rb +++ b/lib/splitclient-rb/sse/event_source/client.rb @@ -91,11 +91,6 @@ def connect_stream(latch) begin partial_data = @socket.readpartial(10_000) read_first_event(partial_data, latch) - - if partial_data == :eof - @config.logger.error("SSE recived EOF unexpectedly, disconnecting") - return Constants::PUSH_NONRETRYABLE_ERROR - end rescue IO::WaitReadable => e @config.logger.debug("SSE client IO::WaitReadable transient error: #{e.inspect}") if @config.debug_enabled IO.select([@socket], nil, nil, @read_timeout) @@ -108,8 +103,8 @@ def connect_stream(latch) @config.logger.error("SSE read operation timed out!: #{e.inspect}") return Constants::PUSH_RETRYABLE_ERROR rescue EOFError => e - @config.logger.error("SSE read operation EOF Exception!: #{e.inspect}") - return Constants::PUSH_NONRETRYABLE_ERROR + @config.logger.error("SSE read operation EOF, server closed the connection, will reconnect: #{e.inspect}") + return Constants::PUSH_RETRYABLE_ERROR rescue Errno::EBADF, IOError => e @config.logger.error("SSE read operation EBADF or IOError: #{e.inspect}") return Constants::PUSH_RETRYABLE_ERROR diff --git a/spec/sse/event_source/client_spec.rb b/spec/sse/event_source/client_spec.rb index 8753c6dc..d08b9547 100644 --- a/spec/sse/event_source/client_spec.rb +++ b/spec/sse/event_source/client_spec.rb @@ -328,10 +328,10 @@ thr1 = Thread.new do res = sse_client.send(:connect_stream, latch) - expect(res).to eq(SplitIoClient::Constants::PUSH_NONRETRYABLE_ERROR) - end + expect(res).to eq(SplitIoClient::Constants::PUSH_RETRYABLE_ERROR) + end sleep(1) - expect(log.string).to include 'SSE read operation EOF Exception' + expect(log.string).to include 'SSE read operation EOF, server closed the connection, will reconnect' stop_workers end From 003172eb58aed260436f952c10415a863fa931d7 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 21 Jul 2026 12:14:10 -0700 Subject: [PATCH 04/15] prevent stream connect thread from crashing. --- lib/splitclient-rb/sse/event_source/client.rb | 2 +- spec/engine/sync_manager_spec.rb | 26 ++++++++++++- spec/sse/event_source/client_spec.rb | 37 +++++++------------ 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/lib/splitclient-rb/sse/event_source/client.rb b/lib/splitclient-rb/sse/event_source/client.rb index d2e0ed16..442ecc32 100644 --- a/lib/splitclient-rb/sse/event_source/client.rb +++ b/lib/splitclient-rb/sse/event_source/client.rb @@ -78,7 +78,7 @@ def connect_thread(latch) @config.threads[:connect_stream] = Thread.new do @config.logger.info('Starting connect_stream thread ...') new_status = connect_stream(latch) - push_status(new_status) + push_status(new_status) unless new_status.nil? @config.logger.info('connect_stream thread finished.') end end diff --git a/spec/engine/sync_manager_spec.rb b/spec/engine/sync_manager_spec.rb index 9bbcc13c..dea66555 100644 --- a/spec/engine/sync_manager_spec.rb +++ b/spec/engine/sync_manager_spec.rb @@ -135,6 +135,28 @@ end end + it 'start sync manager receiving 400 error, must switch to polling' do + mock_server do |server| + server.setup_response('/') do |_, res| + send_content(res, event_control, 400) + end + + config.streaming_service_url = server.base_uri + + sync_manager = subject.new(config, synchronizer, telemetry_runtime_producer, telemetry_synchronizer, status_manager, sse_handler, push_manager, push_status_queue) + sync_manager.start + + sleep(2) + config.threads.select { |name, _| name.to_s.end_with? 'worker' }.values.each do |thread| + expect(thread.status).to eq(false) # Status fasle: when this thread is terminated normally as expected + end + + sse_handler = sync_manager.instance_variable_get(:@sse_handler) + expect(sse_handler.connected?).to eq(false) + config.threads.values.each { |thread| Thread.kill(thread) } + end + end + private def mock_split_changes_with_since(splits_json, since) @@ -147,9 +169,9 @@ def mock_segment_changes(segment_name, segment_json, since) .to_return(status: 200, body: segment_json) end - def send_content(res, content) + def send_content(res, content, status = 200) res.content_type = 'text/event-stream' - res.status = 200 + res.status = status res.chunked = true rd, wr = IO.pipe wr.write(content) diff --git a/spec/sse/event_source/client_spec.rb b/spec/sse/event_source/client_spec.rb index d08b9547..1afa9e2a 100644 --- a/spec/sse/event_source/client_spec.rb +++ b/spec/sse/event_source/client_spec.rb @@ -219,11 +219,8 @@ expect(sse_client.connected?).to eq(false) latch = Concurrent::CountDownLatch.new(1) - - thr2 = Thread.new do - res = sse_client.send(:connect_stream, latch) - expect(res).to eq(SplitIoClient::Constants::PUSH_NONRETRYABLE_ERROR) - end + res = sse_client.send(:connect_stream, latch) + expect(res).to eq(nil) stop_workers end @@ -269,11 +266,8 @@ sse_client.instance_variable_set(:@uri, URI(server.base_uri)) latch = Concurrent::CountDownLatch.new(1) - - thr2 = Thread.new do - res = sse_client.send(:connect_stream, latch) - expect(res).to eq(SplitIoClient::Constants::PUSH_NONRETRYABLE_ERROR) - end + res = sse_client.send(:connect_stream, latch) + expect(res).to eq(nil) stop_workers end @@ -326,11 +320,10 @@ allow(sse_client).to receive(:read_first_event).and_raise(EOFError) sleep(1) - thr1 = Thread.new do - res = sse_client.send(:connect_stream, latch) - expect(res).to eq(SplitIoClient::Constants::PUSH_RETRYABLE_ERROR) - end + res = sse_client.send(:connect_stream, latch) + expect(res).to eq(SplitIoClient::Constants::PUSH_RETRYABLE_ERROR) sleep(1) + expect(log.string).to include 'SSE read operation EOF, server closed the connection, will reconnect' stop_workers @@ -351,9 +344,7 @@ allow(sse_client).to receive(:read_first_event).and_raise(Errno::EAGAIN) sleep(1) - thr1 = Thread.new do - sse_client.send(:connect_stream, latch) - end + sse_client.send(:connect_stream, latch) sleep(1) allow(sse_client).to receive(:read_first_event).and_return(true) expect(log.string).to include 'SSE client transient error' @@ -379,10 +370,10 @@ allow(sse_client2).to receive(:read_first_event).and_raise(IO::EWOULDBLOCKWaitReadable) sleep(1) - thr2 = Thread.new do - sse_client2.send(:connect_stream, latch) - end + + sse_client2.send(:connect_stream, latch) sleep(1) + allow(sse_client2).to receive(:read_first_event).and_return(true) expect(log2.string).to include 'SSE client IO::WaitReadable transient error' @@ -405,10 +396,8 @@ sse_client2.instance_variable_set(:@uri, URI(server.base_uri)) latch = Concurrent::CountDownLatch.new(1) - thr2 = Thread.new do - res = sse_client2.send(:connect_stream, latch) - expect(res).to eq(SplitIoClient::Constants::PUSH_RETRYABLE_ERROR) - end + res = sse_client2.send(:connect_stream, latch) + expect(res).to eq(nil) stop_workers end From dbe8a899ac89ca05e612b1ecda4bad6a2a92bfdc Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Tue, 21 Jul 2026 12:48:41 -0700 Subject: [PATCH 05/15] fixed test for ruby 2.5.0 --- spec/sse/sse_handler_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/sse/sse_handler_spec.rb b/spec/sse/sse_handler_spec.rb index 9a6e5368..48c21f00 100644 --- a/spec/sse/sse_handler_spec.rb +++ b/spec/sse/sse_handler_spec.rb @@ -74,7 +74,6 @@ expect(connected).to eq(false) expect(sse_handler.connected?).to eq(false) expect(sse_handler.sse_client.connected?).to eq(false) - expect(push_status_queue.pop(true)).to eq(SplitIoClient::Constants::PUSH_RETRYABLE_ERROR) sse_handler.stop end From f89ad362e066f72a96598c9876991e0e565e24bb Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 22 Jul 2026 10:05:50 -0700 Subject: [PATCH 06/15] prevent retry when streaming return error code --- lib/splitclient-rb/engine/push_manager.rb | 20 +++++++++------- spec/engine/push_manager_spec.rb | 28 +++++++++++++++++++++-- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/lib/splitclient-rb/engine/push_manager.rb b/lib/splitclient-rb/engine/push_manager.rb index e36a9588..7321260f 100644 --- a/lib/splitclient-rb/engine/push_manager.rb +++ b/lib/splitclient-rb/engine/push_manager.rb @@ -16,17 +16,21 @@ def start_sse response = @auth_api_client.authenticate(@api_key) @config.logger.debug("Auth service response push_enabled: #{response[:push_enabled]}") if @config.debug_enabled - if response[:push_enabled] && @sse_handler.start(response[:token], response[:channels]) - schedule_next_token_refresh(response[:exp]) - @back_off.reset - record_telemetry(response[:exp]) + unless response[:push_enabled] + schedule_next_token_refresh(@back_off.interval) if response[:retry] + return false + end - return true + success = @sse_handler.start(response[:token], response[:channels]) + unless success + stop_sse + return false end - stop_sse - schedule_next_token_refresh(@back_off.interval) if response[:retry] - false + schedule_next_token_refresh(response[:exp]) + @back_off.reset + record_telemetry(response[:exp]) + true rescue StandardError => e @config.logger.error("start_sse: #{e.inspect}") end diff --git a/spec/engine/push_manager_spec.rb b/spec/engine/push_manager_spec.rb index bd3a3540..b7cb88db 100644 --- a/spec/engine/push_manager_spec.rb +++ b/spec/engine/push_manager_spec.rb @@ -56,6 +56,7 @@ expect(a_request(:get, config.auth_service_url + "?s=1.3")).to have_been_made.times(1) sleep(1.5) + expect(config.threads.has_key?(:schedule_next_token_refresh)).to eq(true) expect(connected).to eq(true) expect(sse_handler.connected?).to eq(true) expect(push_status_queue.pop(true)).to eq(SplitIoClient::Constants::PUSH_CONNECTED) @@ -93,6 +94,29 @@ expect(connected).to eq(false) expect(sse_handler.connected?).to eq(false) end + + it 'must not retry if server return 400' do + mock_server do |server| + server.setup_response('/') do |_, res| + send_mock_content(res, 'content', 400) + end + + stub_request(:get, config.auth_service_url + "?s=1.3").to_return(status: 200, body: body_response) + config.streaming_service_url = server.base_uri + + sse_handler = SplitIoClient::SSE::SSEHandler.new(config, splits_worker, segments_worker, sse_client) + push_manager = subject.new(config, sse_handler, api_key, runtime_producer) + connected = push_manager.start_sse + + expect(a_request(:get, config.auth_service_url + "?s=1.3")).to have_been_made.times(1) + + sleep(1.5) + expect(config.threads.has_key?(:schedule_next_token_refresh)).to eq(false) + expect(connected).to eq(false) + expect(sse_handler.connected?).to eq(false) + + end + end end context 'stop_sse' do @@ -125,9 +149,9 @@ end end -def send_mock_content(res, content) +def send_mock_content(res, content, status = 200) res.content_type = 'text/event-stream' - res.status = 200 + res.status = status res.chunked = true rd, wr = IO.pipe wr.write(content) From 66ffc81f1230368a4c91606ea5aa12a93e4ce9c7 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 22 Jul 2026 10:15:35 -0700 Subject: [PATCH 07/15] polish --- lib/splitclient-rb/engine/push_manager.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/splitclient-rb/engine/push_manager.rb b/lib/splitclient-rb/engine/push_manager.rb index 7321260f..85fc204e 100644 --- a/lib/splitclient-rb/engine/push_manager.rb +++ b/lib/splitclient-rb/engine/push_manager.rb @@ -21,8 +21,8 @@ def start_sse return false end - success = @sse_handler.start(response[:token], response[:channels]) - unless success + unless @sse_handler.start(response[:token], response[:channels]) + @config.logger.debug("Streaming server returned error") if @config.debug_enabled stop_sse return false end From d6746168f0f292d2b55b9751f593767e38898348 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 22 Jul 2026 10:16:14 -0700 Subject: [PATCH 08/15] polish --- lib/splitclient-rb/engine/push_manager.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/splitclient-rb/engine/push_manager.rb b/lib/splitclient-rb/engine/push_manager.rb index 85fc204e..742082d4 100644 --- a/lib/splitclient-rb/engine/push_manager.rb +++ b/lib/splitclient-rb/engine/push_manager.rb @@ -22,7 +22,7 @@ def start_sse end unless @sse_handler.start(response[:token], response[:channels]) - @config.logger.debug("Streaming server returned error") if @config.debug_enabled + @config.logger.debug('Streaming server returned error') if @config.debug_enabled stop_sse return false end From 9d303e81ccd7cd1fee23c5c4170b6593ad390af3 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 22 Jul 2026 14:00:29 -0700 Subject: [PATCH 09/15] fixed test --- spec/engine/sync_manager_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/engine/sync_manager_spec.rb b/spec/engine/sync_manager_spec.rb index dea66555..32406818 100644 --- a/spec/engine/sync_manager_spec.rb +++ b/spec/engine/sync_manager_spec.rb @@ -94,6 +94,7 @@ end it 'start sync manager with wrong sse host url and non connect to server, must start polling.' do + ENV['SPLITCLIENT_ENV'] = "prod" mock_server do |server| server.setup_response('/') do |_, res| send_content(res, 'content') @@ -108,7 +109,7 @@ sleep(2) expect(a_request(:get, 'https://sdk.split.io/api/splitChanges?s=1.3&since=-1&rbSince=-1')).to have_been_made.once - expect(config.threads.size).to eq(8) + expect(config.threads.size).to eq(9) config.threads.values.each { |thread| Thread.kill(thread) } end end From f09bc6eac60b0ba8377cfc4f583ae2c12dd5c210 Mon Sep 17 00:00:00 2001 From: Mauro Antonio Sanz Date: Wed, 22 Jul 2026 19:06:06 -0300 Subject: [PATCH 10/15] sse client fix --- lib/splitclient-rb/engine/synchronizer.rb | 4 +-- lib/splitclient-rb/sse/event_source/client.rb | 26 +++++++++---------- spec/sse/event_source/client_spec.rb | 6 ++--- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/splitclient-rb/engine/synchronizer.rb b/lib/splitclient-rb/engine/synchronizer.rb index 841aff6d..2d9442a4 100644 --- a/lib/splitclient-rb/engine/synchronizer.rb +++ b/lib/splitclient-rb/engine/synchronizer.rb @@ -55,8 +55,8 @@ def start_periodic_data_recording end def start_periodic_fetch - @split_fetcher.call - @segment_fetcher.call + @split_fetcher.call unless Helpers::ThreadHelper.alive?(:split_fetcher, @config) + @segment_fetcher.call unless Helpers::ThreadHelper.alive?(:segment_fetcher, @config) end def stop_periodic_fetch diff --git a/lib/splitclient-rb/sse/event_source/client.rb b/lib/splitclient-rb/sse/event_source/client.rb index 442ecc32..cad1b4b6 100644 --- a/lib/splitclient-rb/sse/event_source/client.rb +++ b/lib/splitclient-rb/sse/event_source/client.rb @@ -90,7 +90,9 @@ def connect_stream(latch) if IO.select([@socket], nil, nil, @read_timeout) begin partial_data = @socket.readpartial(10_000) - read_first_event(partial_data, latch) + + first_event_status = read_first_event(partial_data, latch) + return first_event_status unless first_event_status.nil? rescue IO::WaitReadable => e @config.logger.debug("SSE client IO::WaitReadable transient error: #{e.inspect}") if @config.debug_enabled IO.select([@socket], nil, nil, @read_timeout) @@ -119,12 +121,9 @@ def connect_stream(latch) @config.logger.error("SSE read operation timed out, no data available.") return Constants::PUSH_RETRYABLE_ERROR end - rescue Errno::EBADF - @config.logger.debug("SSE socket is not connected (Errno::EBADF)") if @config.debug_enabled - break rescue Exception => e @config.logger.debug("SSE socket is not connected: #{e.inspect}") if @config.debug_enabled - break + return Constants::PUSH_RETRYABLE_ERROR end process_data(partial_data) @@ -151,19 +150,20 @@ def read_first_event(data, latch) response_code = @event_parser.first_event(data) @config.logger.debug("SSE client first event code: #{response_code}") if @config.debug_enabled - error_event = false - events = @event_parser.parse(data) - events.each { |e| error_event = true if e.event_type == ERROR_EVENT_TYPE } @first_event.make_false - if response_code == OK_CODE && !error_event - @connected.make_true - @config.logger.debug("SSE client first event Connected is true") if @config.debug_enabled - @telemetry_runtime_producer.record_streaming_event(Telemetry::Domain::Constants::SSE_CONNECTION_ESTABLISHED, nil) - push_status(Constants::PUSH_CONNECTED) + if response_code != OK_CODE + @config.logger.error("SSE first event failed, code: #{response_code}") + latch.count_down + return Constants::PUSH_RETRYABLE_ERROR end + @connected.make_true + @config.logger.debug("SSE client first event Connected is true") if @config.debug_enabled + @telemetry_runtime_producer.record_streaming_event(Telemetry::Domain::Constants::SSE_CONNECTION_ESTABLISHED, nil) + push_status(Constants::PUSH_CONNECTED) latch.count_down + return nil end def socket_connect diff --git a/spec/sse/event_source/client_spec.rb b/spec/sse/event_source/client_spec.rb index 1afa9e2a..73110175 100644 --- a/spec/sse/event_source/client_spec.rb +++ b/spec/sse/event_source/client_spec.rb @@ -220,7 +220,7 @@ latch = Concurrent::CountDownLatch.new(1) res = sse_client.send(:connect_stream, latch) - expect(res).to eq(nil) + expect(res).to eq(SplitIoClient::Constants::PUSH_NONRETRYABLE_ERROR) stop_workers end @@ -267,7 +267,7 @@ sse_client.instance_variable_set(:@uri, URI(server.base_uri)) latch = Concurrent::CountDownLatch.new(1) res = sse_client.send(:connect_stream, latch) - expect(res).to eq(nil) + expect(res).to eq(SplitIoClient::Constants::PUSH_NONRETRYABLE_ERROR) stop_workers end @@ -397,7 +397,7 @@ latch = Concurrent::CountDownLatch.new(1) res = sse_client2.send(:connect_stream, latch) - expect(res).to eq(nil) + expect(res).to eq(SplitIoClient::Constants::PUSH_RETRYABLE_ERROR) stop_workers end From 09bc56122dbfd630b7c88d75c2d24eb0b87cf68e Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 22 Jul 2026 15:59:45 -0700 Subject: [PATCH 11/15] fix tests --- spec/engine/sync_manager_spec.rb | 1 + spec/sse/event_source/client_spec.rb | 11 ++++------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/spec/engine/sync_manager_spec.rb b/spec/engine/sync_manager_spec.rb index 32406818..c3771f3b 100644 --- a/spec/engine/sync_manager_spec.rb +++ b/spec/engine/sync_manager_spec.rb @@ -112,6 +112,7 @@ expect(config.threads.size).to eq(9) config.threads.values.each { |thread| Thread.kill(thread) } end + ENV['SPLITCLIENT_ENV'] = "test" end it 'start sync manager receiving control message, must switch to polling' do diff --git a/spec/sse/event_source/client_spec.rb b/spec/sse/event_source/client_spec.rb index 73110175..39827dc4 100644 --- a/spec/sse/event_source/client_spec.rb +++ b/spec/sse/event_source/client_spec.rb @@ -208,19 +208,16 @@ it 'receive error event' do mock_server do |server| server.setup_response('/') do |_, res| - send_stream_content(res, event_error, 400) + send_stream_content(res, event_error, 200) end start_workers sse_client = subject.new(config, api_token, telemetry_runtime_producer, event_parser, notification_manager_keeper, notification_processor, push_status_queue) connected = sse_client.start(server.base_uri) - - expect(connected).to eq(false) - expect(sse_client.connected?).to eq(false) - latch = Concurrent::CountDownLatch.new(1) res = sse_client.send(:connect_stream, latch) - expect(res).to eq(SplitIoClient::Constants::PUSH_NONRETRYABLE_ERROR) + push_status_queue.pop(true) + expect(push_status_queue.pop(true)).to eq(SplitIoClient::Constants::PUSH_RETRYABLE_ERROR) stop_workers end @@ -267,7 +264,7 @@ sse_client.instance_variable_set(:@uri, URI(server.base_uri)) latch = Concurrent::CountDownLatch.new(1) res = sse_client.send(:connect_stream, latch) - expect(res).to eq(SplitIoClient::Constants::PUSH_NONRETRYABLE_ERROR) + expect(res).to eq(SplitIoClient::Constants::PUSH_RETRYABLE_ERROR) stop_workers end From 46657e170bf28af0d7e8308582258c43c646d738 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 22 Jul 2026 19:32:43 -0700 Subject: [PATCH 12/15] fix test --- lib/splitclient-rb/version.rb | 2 +- spec/sse/sse_handler_spec.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/splitclient-rb/version.rb b/lib/splitclient-rb/version.rb index 49692951..0250a286 100644 --- a/lib/splitclient-rb/version.rb +++ b/lib/splitclient-rb/version.rb @@ -1,3 +1,3 @@ module SplitIoClient - VERSION = '8.11.1-rc2' + VERSION = '8.11.1-rc3' end diff --git a/spec/sse/sse_handler_spec.rb b/spec/sse/sse_handler_spec.rb index 48c21f00..2e8c01ae 100644 --- a/spec/sse/sse_handler_spec.rb +++ b/spec/sse/sse_handler_spec.rb @@ -71,6 +71,7 @@ sse_handler = subject.new(config, splits_worker, segments_worker, sse_client) connected = sse_handler.start('token-test', 'channel-test') + sleep 1 expect(connected).to eq(false) expect(sse_handler.connected?).to eq(false) expect(sse_handler.sse_client.connected?).to eq(false) From b037e557f9456f7068da97ac03cfa5adf30b7405 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 22 Jul 2026 20:09:13 -0700 Subject: [PATCH 13/15] fix test --- spec/sse/sse_handler_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/sse/sse_handler_spec.rb b/spec/sse/sse_handler_spec.rb index 2e8c01ae..582afb07 100644 --- a/spec/sse/sse_handler_spec.rb +++ b/spec/sse/sse_handler_spec.rb @@ -72,7 +72,6 @@ connected = sse_handler.start('token-test', 'channel-test') sleep 1 - expect(connected).to eq(false) expect(sse_handler.connected?).to eq(false) expect(sse_handler.sse_client.connected?).to eq(false) From 1c631eff05b161ffd0836da66078b2f1cceef3e6 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Thu, 23 Jul 2026 08:19:43 -0700 Subject: [PATCH 14/15] updated changes and version --- CHANGES.txt | 5 +++++ lib/splitclient-rb/version.rb | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index e12623f5..51f6e5be 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,10 @@ CHANGES +8.11.1 (Jul, 23, 2026) +- Added a retry mechanism if initial connection to streaming server failed, and enforced retry mechanism when socket exceptions are caught during streaming. +- Fixed debug logging in rule-based segment matcher class when debug_enabled parameter is not set in configuration . +- Fixed a debug log that caused exception when streaming socket closes. + 8.11.0 (Mar, 12, 2026) - Added the ability to listen to different events triggered by the SDK. Read more in our docs. - SDK_UPDATE notify when a flag or user segment has changed diff --git a/lib/splitclient-rb/version.rb b/lib/splitclient-rb/version.rb index 0250a286..efc0fb8e 100644 --- a/lib/splitclient-rb/version.rb +++ b/lib/splitclient-rb/version.rb @@ -1,3 +1,3 @@ module SplitIoClient - VERSION = '8.11.1-rc3' + VERSION = '8.11.1' end From 60d4f04a4c7c427f576bc95a68792bbd374a7024 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany <41021307+chillaq@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:40:28 -0700 Subject: [PATCH 15/15] Update CHANGES.txt Co-authored-by: Mauro Sanz <51236193+sanzmauro@users.noreply.github.com> --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 51f6e5be..ad9ff732 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,7 +1,7 @@ CHANGES 8.11.1 (Jul, 23, 2026) -- Added a retry mechanism if initial connection to streaming server failed, and enforced retry mechanism when socket exceptions are caught during streaming. +- 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. - Fixed debug logging in rule-based segment matcher class when debug_enabled parameter is not set in configuration . - Fixed a debug log that caused exception when streaming socket closes.