Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions lib/mixpanel-ruby/flags/local_flags_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,17 @@ def start_polling_for_definitions!
# blocking HTTP call, and holding the lifecycle lock across it would
# block a concurrent stop_polling_for_definitions! for the full request
# timeout.
fetch_flag_definitions
#
# A transient failure here (network blip, HTTP 500) should NOT prevent
# the polling thread from spawning — the loop retries on the configured
# interval. Without this inner rescue, the outer rescue below would
# catch and return, leaving the SDK permanently without polling until
# the caller manually retried.
begin
fetch_flag_definitions
rescue StandardError => e
safe_handle_error(e)
end

@lifecycle_mutex.synchronize do
# If a stop arrived during/after our @stop_polling clear above, abort
Expand Down Expand Up @@ -202,11 +212,19 @@ def get_all_variants(context)
# Wrap @error_handler.handle so a misbehaving handler can't kill the
# polling thread mid-loop — that would leave @polling_thread non-nil but
# dead, and (without the .alive? check in start) silently prevent restart.
#
# Always warn to stderr as well. The default Mixpanel::ErrorHandler#handle
# is a no-op, so dispatching only via @error_handler swallows schema drift
# (NoMethodError, JSON::ParserError, etc.) — the loop runs forever
# undetected. Matches the convention in mixpanel-python / mixpanel-java /
# mixpanel-go / mixpanel-node, all of which log unconditionally and keep
# polling.
def safe_handle_error(error)
warn "[Mixpanel] Failed to fetch flag definitions: #{error.class}: #{error.message}"
@error_handler.handle(error) if @error_handler
rescue StandardError
# Swallow: keeping the polling loop alive is more important than
# propagating a broken handler's failure.
# Swallow handler failures: keeping the polling loop alive is more
# important than propagating a broken handler's failure.
end

def fetch_flag_definitions
Expand Down
48 changes: 48 additions & 0 deletions spec/mixpanel-ruby/flags/local_flags_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,54 @@ def user_context_with_properties(properties)
polling_provider.stop_polling_for_definitions!
end
end

# SDK-78: safe_handle_error previously dispatched only to @error_handler,
# whose default Mixpanel::ErrorHandler#handle is a no-op — schema drift
# (NoMethodError, JSON::ParserError, etc.) looped forever undetected.
# Warn to stderr unconditionally so failures are visible without an
# error_handler being configured.
it 'warns to stderr when fetch raises and no error_handler is configured' do
stub_request(:get, endpoint_url_regex).to_return(status: 500, body: 'server down')

polling_provider = Mixpanel::Flags::LocalFlagsProvider.new(
test_token,
{ enable_polling: false },
mock_tracker,
nil
)

expect { polling_provider.start_polling_for_definitions! }
.to output(/\[Mixpanel\] Failed to fetch flag definitions: Mixpanel::ServerError/).to_stderr
end

# Regression: previously the outer rescue on start_polling_for_definitions!
# caught an initial-fetch failure and returned before the @lifecycle_mutex
# block ever spawned the poller thread. A single startup blip (transient
# 500 / network timeout) left the SDK permanently without a poller until
# the caller retried — the whole point of polling.
it 'still spawns the polling thread when the initial fetch fails' do
stub_request(:get, endpoint_url_regex).to_return(status: 500, body: 'transient')

polling_provider = Mixpanel::Flags::LocalFlagsProvider.new(
test_token,
{ enable_polling: true, polling_interval_in_seconds: 30 },
mock_tracker,
nil
)

begin
# Warning is expected because the initial fetch fails; capture stderr
# so it doesn't pollute test output.
expect { polling_provider.start_polling_for_definitions! }
.to output(/\[Mixpanel\] Failed to fetch flag definitions/).to_stderr

polling_thread = polling_provider.instance_variable_get(:@polling_thread)
expect(polling_thread).not_to be_nil
expect(polling_thread).to be_alive
ensure
polling_provider.stop_polling_for_definitions!
end
end
end

describe 'service account credentials' do
Expand Down
Loading