diff --git a/lib/ldclient-rb/ldclient.rb b/lib/ldclient-rb/ldclient.rb index 36388536..bbe65e48 100644 --- a/lib/ldclient-rb/ldclient.rb +++ b/lib/ldclient-rb/ldclient.rb @@ -81,6 +81,10 @@ def initialize(sdk_key, config = Config.default, wait_for_sec = 5) # The pid of the process in which the fork warning was last logged, or nil. @fork_warned_pid = Concurrent::AtomicReference.new(nil) + # Each flag lets the matching cached-data warning log once per client. + @cached_data_evaluation_warned = Concurrent::AtomicBoolean.new(false) + @cached_data_all_flags_warned = Concurrent::AtomicBoolean.new(false) + start_up(wait_for_sec) end @@ -629,7 +633,9 @@ def all_flags_state(context, options={}) unless initialized? if @data_system.store.initialized? - @config.logger.warn { "Called all_flags_state before client initialization; using last known values from data store" } + if @cached_data_all_flags_warned.make_true + @config.logger.warn { "Called all_flags_state before client initialization; using last known values from data store. This message is logged once." } + end else @config.logger.warn { "Called all_flags_state before client initialization. Data store not available; returning empty state" } return FeatureFlagsState.new(false) @@ -765,7 +771,9 @@ def flag_tracker if @data_system.data_availability != Impl::DataSystem::DataAvailability::REFRESHED if @data_system.data_availability == Impl::DataSystem::DataAvailability::CACHED - @config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" } + if @cached_data_evaluation_warned.make_true + @config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store. This message is logged once." } + end else @config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" } detail = Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default) diff --git a/spec/ldclient_cached_data_warning_spec.rb b/spec/ldclient_cached_data_warning_spec.rb new file mode 100644 index 00000000..de0c983b --- /dev/null +++ b/spec/ldclient_cached_data_warning_spec.rb @@ -0,0 +1,82 @@ +require "capturing_logger" +require "mock_components" +require "spec_helper" + +module LaunchDarkly + describe "LDClient cached-data warnings" do + let(:logger) { CapturingLogger.new } + let(:context) { basic_context } + + # A data source that starts but never reports initialized. With a store that already holds + # data, the client evaluates with cached data. + def uninitialized_source + wait = double + allow(wait).to receive(:wait) + source = double + allow(source).to receive(:start).and_return(wait) + allow(source).to receive(:stop) + allow(source).to receive(:initialized?).and_return(false) + source + end + + def initialized_store + store = InMemoryFeatureStore.new + store.init({ Impl::DataStore::FEATURES => {}, Impl::DataStore::SEGMENTS => {} }) + store + end + + def cached_data_config + test_config(logger: logger, feature_store: initialized_store, data_source: uninitialized_source) + end + + def evaluation_warnings(logger) + logger.output.lines.grep(/Client has not finished initializing; using last known values/) + end + + def all_flags_state_warnings(logger) + logger.output.lines.grep(/Called all_flags_state before client initialization; using last known values/) + end + + it "logs the evaluation warning once per client" do + with_client(cached_data_config) do |client| + client.variation("flag", context, false) + client.variation("flag", context, false) + client.variation_detail("flag", context, false) + expect(evaluation_warnings(logger).length).to eq(1) + end + end + + # all_flags_state checks LDClient#initialized?, which is true when the store holds data. The + # stub makes the client report not initialized so the cached-data branch runs. + it "logs the all_flags_state warning once per client" do + with_client(cached_data_config) do |client| + allow(client).to receive(:initialized?).and_return(false) + client.all_flags_state(context) + client.all_flags_state(context) + expect(all_flags_state_warnings(logger).length).to eq(1) + end + end + + it "logs the evaluation and all_flags_state warnings independently" do + with_client(cached_data_config) do |client| + allow(client).to receive(:initialized?).and_return(false) + client.variation("flag", context, false) + client.all_flags_state(context) + client.variation("flag", context, false) + client.all_flags_state(context) + expect(evaluation_warnings(logger).length).to eq(1) + expect(all_flags_state_warnings(logger).length).to eq(1) + end + end + + it "logs the warning again for a new client" do + with_client(cached_data_config) do |client| + client.variation("flag", context, false) + end + with_client(cached_data_config) do |client| + client.variation("flag", context, false) + end + expect(evaluation_warnings(logger).length).to eq(2) + end + end +end