From 66b887697ba869555ed939fd491413576968ac43 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Tue, 2 Dec 2025 22:23:09 +0000 Subject: [PATCH 01/10] chore: Create FDv1 datasystem implementation --- .../impl/datasource/null_processor.rb | 53 ++++ lib/ldclient-rb/impl/datasystem.rb | 18 +- lib/ldclient-rb/impl/datasystem/fdv1.rb | 227 +++++++++++++++ lib/ldclient-rb/ldclient.rb | 24 +- spec/impl/datasystem/fdv1_spec.rb | 267 ++++++++++++++++++ spec/impl/datasystem_spec.rb | 8 +- spec/mock_components.rb | 34 ++- 7 files changed, 603 insertions(+), 28 deletions(-) create mode 100644 lib/ldclient-rb/impl/datasource/null_processor.rb create mode 100644 lib/ldclient-rb/impl/datasystem/fdv1.rb create mode 100644 spec/impl/datasystem/fdv1_spec.rb diff --git a/lib/ldclient-rb/impl/datasource/null_processor.rb b/lib/ldclient-rb/impl/datasource/null_processor.rb new file mode 100644 index 00000000..39da60d1 --- /dev/null +++ b/lib/ldclient-rb/impl/datasource/null_processor.rb @@ -0,0 +1,53 @@ +require 'concurrent' + +module LaunchDarkly + module Impl + module DataSource + # + # A minimal UpdateProcessor implementation used when the SDK is in offline mode + # or daemon (LDD) mode. It does nothing except mark itself as initialized. + # + # @private + # + class NullUpdateProcessor + include LaunchDarkly::Interfaces::DataSource + + # + # Creates a new NullUpdateProcessor. + # + def initialize + @ready = Concurrent::Event.new + end + + # + # Starts the data source. Since this is a null implementation, it immediately + # sets the ready event to indicate initialization is complete. + # + # @return [Concurrent::Event] The ready event + # + def start + @ready.set + @ready + end + + # + # Stops the data source. This is a no-op for the null implementation. + # + # @return [void] + # + def stop + # Nothing to do + end + + # + # Checks if the data source has been initialized. + # + # @return [Boolean] Always returns true since this is a null implementation + # + def initialized? + true + end + end + end + end +end diff --git a/lib/ldclient-rb/impl/datasystem.rb b/lib/ldclient-rb/impl/datasystem.rb index a3583d5c..44aa5eb3 100644 --- a/lib/ldclient-rb/impl/datasystem.rb +++ b/lib/ldclient-rb/impl/datasystem.rb @@ -19,13 +19,12 @@ module DataSystem # # Starts the data system. # - # This method will return immediately. The provided event will be set when the system + # This method will return immediately. The returned event will be set when the system # has reached an initial state (either permanently failed, e.g. due to bad auth, or succeeded). # - # @param ready_event [Concurrent::Event] Event to set when initialization is complete - # @return [void] + # @return [Concurrent::Event] Event that will be set when initialization is complete # - def start(ready_event) + def start raise NotImplementedError, "#{self.class} must implement #start" end @@ -117,6 +116,17 @@ def set_flag_value_eval_fn(eval_fn) raise NotImplementedError, "#{self.class} must implement #set_flag_value_eval_fn" end + # + # Sets the diagnostic accumulator for streaming initialization metrics. + # This should be called before start() to ensure metrics are collected. + # + # @param diagnostic_accumulator [DiagnosticAccumulator] The diagnostic accumulator + # @return [void] + # + def set_diagnostic_accumulator(diagnostic_accumulator) + raise NotImplementedError, "#{self.class} must implement #set_diagnostic_accumulator" + end + # # Represents the availability of data in the SDK. # diff --git a/lib/ldclient-rb/impl/datasystem/fdv1.rb b/lib/ldclient-rb/impl/datasystem/fdv1.rb new file mode 100644 index 00000000..1a3c0ade --- /dev/null +++ b/lib/ldclient-rb/impl/datasystem/fdv1.rb @@ -0,0 +1,227 @@ +require 'concurrent' +require 'ldclient-rb/impl/datasystem' +require 'ldclient-rb/impl/data_source' +require 'ldclient-rb/impl/data_store' +require 'ldclient-rb/impl/datasource/null_processor' +require 'ldclient-rb/impl/flag_tracker' +require 'ldclient-rb/impl/broadcaster' + +module LaunchDarkly + module Impl + module DataSystem + # + # FDv1 wires the existing v1 data source and store behavior behind the + # generic DataSystem surface. + # + # @private + # + class FDv1 + include LaunchDarkly::Impl::DataSystem + + # + # Creates a new FDv1 data system. + # + # @param sdk_key [String] The SDK key + # @param config [LaunchDarkly::Config] The SDK configuration + # + def initialize(sdk_key, config) + @sdk_key = sdk_key + @config = config + @shared_executor = Concurrent::SingleThreadExecutor.new + + # Set up data store plumbing + @data_store_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @config.logger) + @data_store_update_sink = LaunchDarkly::Impl::DataStore::UpdateSink.new( + @data_store_broadcaster + ) + + # Wrap the data store with client wrapper (must be created before status provider) + @store_wrapper = LaunchDarkly::Impl::FeatureStoreClientWrapper.new( + @config.feature_store, + @data_store_update_sink, + @config.logger + ) + + # Create status provider with store wrapper + @data_store_status_provider = LaunchDarkly::Impl::DataStore::StatusProvider.new( + @store_wrapper, + @data_store_update_sink + ) + + # Set up data source plumbing + @data_source_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @config.logger) + @flag_change_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @config.logger) + @flag_tracker_impl = LaunchDarkly::Impl::FlagTracker.new( + @flag_change_broadcaster, + lambda { |_key, _context| nil } # Replaced by client to use its evaluation method + ) + @data_source_update_sink = LaunchDarkly::Impl::DataSource::UpdateSink.new( + @store_wrapper, + @data_source_broadcaster, + @flag_change_broadcaster + ) + @data_source_status_provider = LaunchDarkly::Impl::DataSource::StatusProvider.new( + @data_source_broadcaster, + @data_source_update_sink + ) + + # Ensure v1 processors can find the sink via config for status updates + @config.data_source_update_sink = @data_source_update_sink + + # Update processor created in start(), because it needs the ready event + @update_processor = nil + + # Diagnostic accumulator provided by client for streaming metrics + @diagnostic_accumulator = nil + end + + # + # Starts the v1 update processor and returns immediately. The returned event + # will be set by the processor upon first successful initialization or upon permanent failure. + # + # @return [Concurrent::Event] Event that will be set when initialization is complete + # + def start + @update_processor = make_update_processor + @update_processor.start + end + + # + # Halts the data system, stopping the update processor and shutting down the executor. + # + # @return [void] + # + def stop + @update_processor&.stop + @shared_executor.shutdown + end + + # + # Returns the feature store wrapper used by this data system. + # + # @return [LaunchDarkly::Impl::DataStore::ClientWrapper] + # + def store + @store_wrapper + end + + # + # Injects the flag value evaluation function used by the flag tracker to + # compute FlagValueChange events. The function signature should be + # (key, context) -> value. + # + # @param eval_fn [Proc] The evaluation function + # @return [void] + # + def set_flag_value_eval_fn(eval_fn) + @flag_tracker_impl = LaunchDarkly::Impl::FlagTracker.new(@flag_change_broadcaster, eval_fn) + end + + # + # Sets the diagnostic accumulator for streaming initialization metrics. + # This should be called before start() to ensure metrics are collected. + # + # @param diagnostic_accumulator [DiagnosticAccumulator] The diagnostic accumulator + # @return [void] + # + def set_diagnostic_accumulator(diagnostic_accumulator) + @diagnostic_accumulator = diagnostic_accumulator + end + + # + # Returns the data source status provider. + # + # @return [LaunchDarkly::Interfaces::DataSource::StatusProvider] + # + def data_source_status_provider + @data_source_status_provider + end + + # + # Returns the data store status provider. + # + # @return [LaunchDarkly::Interfaces::DataStore::StatusProvider] + # + def data_store_status_provider + @data_store_status_provider + end + + # + # Returns the flag tracker. + # + # @return [LaunchDarkly::Interfaces::FlagTracker] + # + def flag_tracker + @flag_tracker_impl + end + + # + # Indicates what form of data is currently available. + # + # This is calculated dynamically based on current system state. + # + # @return [Symbol] One of DataAvailability constants + # + def data_availability + if @config.offline? + return DataAvailability::DEFAULTS + end + + if @update_processor && @update_processor.initialized? + return DataAvailability::REFRESHED + end + + if @store_wrapper.initialized? + return DataAvailability::CACHED + end + + DataAvailability::DEFAULTS + end + + # + # Indicates the ideal form of data attainable given the current configuration. + # + # @return [Symbol] One of DataAvailability constants + # + def target_availability + if @config.offline? + return DataAvailability::DEFAULTS + end + # In LDD mode or normal connected modes, the ideal is to be refreshed + DataAvailability::REFRESHED + end + + # + # Creates the appropriate update processor based on the configuration. + # + # @return [Object] The update processor + # + private def make_update_processor + # Handle custom data source (factory or instance) + if @config.data_source + return @config.data_source unless @config.data_source.respond_to?(:call) + + # Factory - call with appropriate arity + return @config.data_source.arity == 3 ? + @config.data_source.call(@sdk_key, @config, @diagnostic_accumulator) : + @config.data_source.call(@sdk_key, @config) + end + + # Create default data source based on config + return LaunchDarkly::Impl::DataSource::NullUpdateProcessor.new if @config.offline? || @config.use_ldd? + + if @config.stream? + require 'ldclient-rb/stream' + return LaunchDarkly::StreamProcessor.new(@sdk_key, @config, @diagnostic_accumulator) + end + + # Polling processor + require 'ldclient-rb/polling' + requestor = LaunchDarkly::Requestor.new(@sdk_key, @config) + LaunchDarkly::PollingProcessor.new(@config, requestor) + end + end + end + end +end + diff --git a/lib/ldclient-rb/ldclient.rb b/lib/ldclient-rb/ldclient.rb index 2c986933..f0dc852b 100644 --- a/lib/ldclient-rb/ldclient.rb +++ b/lib/ldclient-rb/ldclient.rb @@ -2,6 +2,7 @@ require "ldclient-rb/impl/broadcaster" require "ldclient-rb/impl/data_source" require "ldclient-rb/impl/data_store" +require "ldclient-rb/impl/datasource/null_processor" require "ldclient-rb/impl/diagnostic_events" require "ldclient-rb/impl/evaluator" require "ldclient-rb/impl/evaluation_with_hook_result" @@ -132,7 +133,7 @@ def postfork(wait_for_sec = 5) if @config.use_ldd? @config.logger.info { "[LDClient] Started LaunchDarkly Client in LDD mode" } - @data_source = NullUpdateProcessor.new + @data_source = LaunchDarkly::Impl::DataSource::NullUpdateProcessor.new return # requestor and update processor are not used in this mode end @@ -710,7 +711,7 @@ def close def create_default_data_source(sdk_key, config, diagnostic_accumulator) if config.offline? - return NullUpdateProcessor.new + return LaunchDarkly::Impl::DataSource::NullUpdateProcessor.new end raise ArgumentError, "sdk_key must not be nil" if sdk_key.nil? # see LDClient constructor comment on sdk_key if config.stream? @@ -877,23 +878,4 @@ def evaluate_internal(key, context, default, with_reasons) false end end - - # - # Used internally when the client is offline. - # @private - # - class NullUpdateProcessor - def start - e = Concurrent::Event.new - e.set - e - end - - def initialized? - true - end - - def stop - end - end end diff --git a/spec/impl/datasystem/fdv1_spec.rb b/spec/impl/datasystem/fdv1_spec.rb new file mode 100644 index 00000000..9996c2f4 --- /dev/null +++ b/spec/impl/datasystem/fdv1_spec.rb @@ -0,0 +1,267 @@ +require "spec_helper" +require "mock_components" +require "ldclient-rb/impl/datasystem/fdv1" + +module LaunchDarkly + module Impl + module DataSystem + describe FDv1 do + let(:sdk_key) { "test-sdk-key" } + let(:config) { LaunchDarkly::Config.new } + subject { FDv1.new(sdk_key, config) } + + describe "#initialize" do + it "creates data store status provider" do + expect(subject.data_store_status_provider).to be_a(LaunchDarkly::Impl::DataStore::StatusProvider) + end + + it "creates data source status provider" do + expect(subject.data_source_status_provider).to be_a(LaunchDarkly::Impl::DataSource::StatusProvider) + end + + it "creates flag tracker" do + expect(subject.flag_tracker).to be_a(LaunchDarkly::Impl::FlagTracker) + end + + it "creates store wrapper" do + expect(subject.store).to be_a(LaunchDarkly::Impl::FeatureStoreClientWrapper) + end + + it "injects data_source_update_sink into config" do + subject # Force creation of FDv1 instance + expect(config.data_source_update_sink).to be_a(LaunchDarkly::Impl::DataSource::UpdateSink) + end + end + + describe "#start" do + it "returns a Concurrent::Event" do + ready_event = subject.start + expect(ready_event).to be_a(Concurrent::Event) + end + + it "creates streaming processor by default" do + allow(LaunchDarkly::StreamProcessor).to receive(:new).and_call_original + subject.start + expect(LaunchDarkly::StreamProcessor).to have_received(:new).with(sdk_key, config, nil) + end + + context "with polling mode" do + let(:config) { LaunchDarkly::Config.new(stream: false) } + + it "creates polling processor" do + allow(LaunchDarkly::PollingProcessor).to receive(:new).and_call_original + subject.start + expect(LaunchDarkly::PollingProcessor).to have_received(:new) + end + end + + context "with offline mode" do + let(:config) { LaunchDarkly::Config.new(offline: true) } + + it "creates null processor" do + expect(LaunchDarkly::Impl::DataSource::NullUpdateProcessor).to receive(:new).and_call_original + ready_event = subject.start + expect(ready_event.set?).to be true + end + end + + context "with LDD mode" do + let(:config) { LaunchDarkly::Config.new(use_ldd: true) } + + it "creates null processor" do + expect(LaunchDarkly::Impl::DataSource::NullUpdateProcessor).to receive(:new).and_call_original + ready_event = subject.start + expect(ready_event.set?).to be true + end + end + + context "with custom data source factory" do + let(:custom_processor) { MockUpdateProcessor.new } + let(:factory) { ->(sdk_key, config, diag) { custom_processor } } + let(:config) { LaunchDarkly::Config.new(data_source: factory) } + + it "calls factory with sdk_key, config, and diagnostic_accumulator" do + expect(factory).to receive(:call).with(sdk_key, config, nil).and_return(custom_processor) + subject.start + end + + it "passes diagnostic_accumulator if set" do + diagnostic_accumulator = double("DiagnosticAccumulator") + subject.set_diagnostic_accumulator(diagnostic_accumulator) + expect(factory).to receive(:call).with(sdk_key, config, diagnostic_accumulator).and_return(custom_processor) + subject.start + end + + context "with arity 2 factory" do + let(:factory) { ->(sdk_key, config) { custom_processor } } + + it "calls factory without diagnostic_accumulator" do + expect(factory).to receive(:call).with(sdk_key, config).and_return(custom_processor) + subject.start + end + end + end + + context "with custom data source instance" do + let(:custom_processor) { MockUpdateProcessor.new } + let(:config) { LaunchDarkly::Config.new(data_source: custom_processor) } + + it "uses the instance directly" do + ready_event = subject.start + expect(ready_event).to be_a(Concurrent::Event) + end + end + end + + describe "#stop" do + it "stops the update processor" do + processor = MockUpdateProcessor.new + allow(subject).to receive(:make_update_processor).and_return(processor) + subject.start + expect(processor).to receive(:stop) + subject.stop + end + + it "shuts down the executor" do + executor = subject.instance_variable_get(:@shared_executor) + expect(executor).to receive(:shutdown) + subject.stop + end + + it "does nothing if not started" do + expect { subject.stop }.not_to raise_error + end + end + + describe "#store" do + it "returns the store wrapper" do + expect(subject.store).to be_a(LaunchDarkly::Impl::FeatureStoreClientWrapper) + end + end + + describe "#set_flag_value_eval_fn" do + it "updates the flag tracker with the evaluation function" do + eval_fn = ->(key, context) { "value" } + original_tracker = subject.flag_tracker + + subject.set_flag_value_eval_fn(eval_fn) + + # Should have created a new flag tracker with the eval function + expect(subject.flag_tracker).to be_a(LaunchDarkly::Impl::FlagTracker) + # The tracker should be different (new instance with eval_fn) + expect(subject.flag_tracker.object_id).not_to eq(original_tracker.object_id) + end + end + + describe "#set_diagnostic_accumulator" do + it "stores the diagnostic accumulator" do + diagnostic_accumulator = double("DiagnosticAccumulator") + expect { subject.set_diagnostic_accumulator(diagnostic_accumulator) }.not_to raise_error + end + end + + describe "#data_source_status_provider" do + it "returns the data source status provider" do + expect(subject.data_source_status_provider).to be_a(LaunchDarkly::Impl::DataSource::StatusProvider) + end + end + + describe "#data_store_status_provider" do + it "returns the data store status provider" do + expect(subject.data_store_status_provider).to be_a(LaunchDarkly::Impl::DataStore::StatusProvider) + end + end + + describe "#flag_tracker" do + it "returns the flag tracker" do + expect(subject.flag_tracker).to be_a(LaunchDarkly::Impl::FlagTracker) + end + end + + describe "#data_availability" do + context "when offline" do + let(:config) { LaunchDarkly::Config.new(offline: true) } + + it "returns DEFAULTS" do + expect(subject.data_availability).to eq(DataAvailability::DEFAULTS) + end + end + + context "when update processor is initialized" do + it "returns REFRESHED" do + processor = MockUpdateProcessor.new + allow(subject).to receive(:make_update_processor).and_return(processor) + subject.start + processor.ready.set + + expect(subject.data_availability).to eq(DataAvailability::REFRESHED) + end + end + + context "when store is initialized but processor is not" do + it "returns CACHED" do + # Initialize the store + subject.store.init({}) + + expect(subject.data_availability).to eq(DataAvailability::CACHED) + end + end + + context "when neither processor nor store are initialized" do + it "returns DEFAULTS" do + expect(subject.data_availability).to eq(DataAvailability::DEFAULTS) + end + end + end + + describe "#target_availability" do + context "when offline" do + let(:config) { LaunchDarkly::Config.new(offline: true) } + + it "returns DEFAULTS" do + expect(subject.target_availability).to eq(DataAvailability::DEFAULTS) + end + end + + context "when not offline" do + it "returns REFRESHED" do + expect(subject.target_availability).to eq(DataAvailability::REFRESHED) + end + end + + context "with LDD mode" do + let(:config) { LaunchDarkly::Config.new(use_ldd: true) } + + it "returns REFRESHED" do + expect(subject.target_availability).to eq(DataAvailability::REFRESHED) + end + end + end + + describe "integration with diagnostic accumulator" do + it "passes diagnostic accumulator to streaming processor" do + diagnostic_accumulator = double("DiagnosticAccumulator") + subject.set_diagnostic_accumulator(diagnostic_accumulator) + + expect(LaunchDarkly::StreamProcessor).to receive(:new).with(sdk_key, config, diagnostic_accumulator).and_call_original + subject.start + end + + context "with polling mode" do + let(:config) { LaunchDarkly::Config.new(stream: false) } + + it "does not pass diagnostic accumulator to polling processor" do + diagnostic_accumulator = double("DiagnosticAccumulator") + subject.set_diagnostic_accumulator(diagnostic_accumulator) + + # PollingProcessor doesn't accept diagnostic_accumulator + expect(LaunchDarkly::PollingProcessor).to receive(:new).with(config, anything).and_call_original + subject.start + end + end + end + end + end + end +end + diff --git a/spec/impl/datasystem_spec.rb b/spec/impl/datasystem_spec.rb index c35425c6..e23347fe 100644 --- a/spec/impl/datasystem_spec.rb +++ b/spec/impl/datasystem_spec.rb @@ -13,8 +13,7 @@ module Impl end it "start raises NotImplementedError" do - ready_event = double("Event") - expect { test_instance.start(ready_event) }.to raise_error(NotImplementedError, /must implement #start/) + expect { test_instance.start }.to raise_error(NotImplementedError, /must implement #start/) end it "stop raises NotImplementedError" do @@ -48,6 +47,11 @@ module Impl it "set_flag_value_eval_fn raises NotImplementedError" do expect { test_instance.set_flag_value_eval_fn(nil) }.to raise_error(NotImplementedError, /must implement #set_flag_value_eval_fn/) end + + it "set_diagnostic_accumulator raises NotImplementedError" do + accumulator = double("DiagnosticAccumulator") + expect { test_instance.set_diagnostic_accumulator(accumulator) }.to raise_error(NotImplementedError, /must implement #set_diagnostic_accumulator/) + end end # Test DataAvailability constants and methods diff --git a/spec/mock_components.rb b/spec/mock_components.rb index 1a4470fa..7e7533ee 100644 --- a/spec/mock_components.rb +++ b/spec/mock_components.rb @@ -2,6 +2,7 @@ require "ldclient-rb/impl/big_segments" require "ldclient-rb/impl/evaluator" +require "ldclient-rb/impl/datasource/null_processor" require "ldclient-rb/interfaces" def sdk_key @@ -9,7 +10,7 @@ def sdk_key end def null_data - LaunchDarkly::NullUpdateProcessor.new + LaunchDarkly::Impl::DataSource::NullUpdateProcessor.new end def null_logger @@ -39,6 +40,37 @@ def basic_context end module LaunchDarkly + class MockUpdateProcessor + attr_reader :ready + + def initialize + @ready = Concurrent::Event.new + @started = false + @stopped = false + end + + def start + @started = true + @ready + end + + def stop + @stopped = true + end + + def initialized? + @ready.set? + end + + def started? + @started + end + + def stopped? + @stopped + end + end + class CapturingFeatureStore attr_reader :received_data From 2763e074784e1d05cd9a266f8c8699cd3c8ffb69 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Wed, 3 Dec 2025 16:38:13 +0000 Subject: [PATCH 02/10] set proper availability for daemon mode --- lib/ldclient-rb/impl/datasystem/fdv1.rb | 19 +++++++------------ spec/impl/datasystem/fdv1_spec.rb | 19 +++++++++++++++++-- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/lib/ldclient-rb/impl/datasystem/fdv1.rb b/lib/ldclient-rb/impl/datasystem/fdv1.rb index 1a3c0ade..343b7d85 100644 --- a/lib/ldclient-rb/impl/datasystem/fdv1.rb +++ b/lib/ldclient-rb/impl/datasystem/fdv1.rb @@ -163,17 +163,13 @@ def flag_tracker # @return [Symbol] One of DataAvailability constants # def data_availability - if @config.offline? - return DataAvailability::DEFAULTS - end + return DataAvailability::DEFAULTS if @config.offline? - if @update_processor && @update_processor.initialized? - return DataAvailability::REFRESHED + unless @config.use_ldd? + return DataAvailability::REFRESHED if @update_processor && @update_processor.initialized? end - if @store_wrapper.initialized? - return DataAvailability::CACHED - end + return DataAvailability::CACHED if @store_wrapper.initialized? DataAvailability::DEFAULTS end @@ -184,10 +180,9 @@ def data_availability # @return [Symbol] One of DataAvailability constants # def target_availability - if @config.offline? - return DataAvailability::DEFAULTS - end - # In LDD mode or normal connected modes, the ideal is to be refreshed + return DataAvailability::DEFAULTS if @config.offline? + return DataAvailability::CACHED if @config.use_ldd? + DataAvailability::REFRESHED end diff --git a/spec/impl/datasystem/fdv1_spec.rb b/spec/impl/datasystem/fdv1_spec.rb index 9996c2f4..77b1e876 100644 --- a/spec/impl/datasystem/fdv1_spec.rb +++ b/spec/impl/datasystem/fdv1_spec.rb @@ -212,6 +212,21 @@ module DataSystem expect(subject.data_availability).to eq(DataAvailability::DEFAULTS) end end + + context "in LDD mode" do + let(:config) { LaunchDarkly::Config.new(use_ldd: true) } + + it "returns DEFAULTS when store is empty" do + subject.start + expect(subject.data_availability).to eq(DataAvailability::DEFAULTS) + end + + it "returns CACHED when store is initialized" do + subject.start + subject.store.init({}) + expect(subject.data_availability).to eq(DataAvailability::CACHED) + end + end end describe "#target_availability" do @@ -232,8 +247,8 @@ module DataSystem context "with LDD mode" do let(:config) { LaunchDarkly::Config.new(use_ldd: true) } - it "returns REFRESHED" do - expect(subject.target_availability).to eq(DataAvailability::REFRESHED) + it "returns CACHED" do + expect(subject.target_availability).to eq(DataAvailability::CACHED) end end end From 473de385fa6cc9d94d38c2fea7892d0c76ce587c Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Wed, 3 Dec 2025 17:08:27 +0000 Subject: [PATCH 03/10] allow flag tracker to be created by calling code The datasystem doesn't use the flagTracker. Instead of creating and being responsible for something it doesn't use, provide access to the broadcaster so the calling code can create a tracker however it needs. --- lib/ldclient-rb/impl/datasystem.rb | 26 +++++------------- lib/ldclient-rb/impl/datasystem/fdv1.rb | 25 +++-------------- spec/impl/datasystem/fdv1_spec.rb | 36 +++---------------------- spec/impl/datasystem_spec.rb | 8 ++---- 4 files changed, 16 insertions(+), 79 deletions(-) diff --git a/lib/ldclient-rb/impl/datasystem.rb b/lib/ldclient-rb/impl/datasystem.rb index 44aa5eb3..4dbf8169 100644 --- a/lib/ldclient-rb/impl/datasystem.rb +++ b/lib/ldclient-rb/impl/datasystem.rb @@ -66,12 +66,15 @@ def data_store_status_provider end # - # Returns an interface for tracking changes in feature flag configurations. + # Returns the broadcaster for flag change notifications. # - # @return [LaunchDarkly::Interfaces::FlagTracker] + # Consumers can use this broadcaster to build their own flag tracker + # or listen for flag changes directly. # - def flag_tracker - raise NotImplementedError, "#{self.class} must implement #flag_tracker" + # @return [LaunchDarkly::Impl::Broadcaster] + # + def flag_change_broadcaster + raise NotImplementedError, "#{self.class} must implement #flag_change_broadcaster" end # @@ -101,21 +104,6 @@ def store raise NotImplementedError, "#{self.class} must implement #store" end - # - # Injects the flag value evaluation function used by the flag tracker to - # compute FlagValueChange events. The function signature should be - # (key, context) -> value. - # - # This method must be called after initialization to enable the flag tracker - # to compute value changes for flag change listeners. - # - # @param eval_fn [Proc] The evaluation function - # @return [void] - # - def set_flag_value_eval_fn(eval_fn) - raise NotImplementedError, "#{self.class} must implement #set_flag_value_eval_fn" - end - # # Sets the diagnostic accumulator for streaming initialization metrics. # This should be called before start() to ensure metrics are collected. diff --git a/lib/ldclient-rb/impl/datasystem/fdv1.rb b/lib/ldclient-rb/impl/datasystem/fdv1.rb index 343b7d85..6ee88a55 100644 --- a/lib/ldclient-rb/impl/datasystem/fdv1.rb +++ b/lib/ldclient-rb/impl/datasystem/fdv1.rb @@ -3,7 +3,6 @@ require 'ldclient-rb/impl/data_source' require 'ldclient-rb/impl/data_store' require 'ldclient-rb/impl/datasource/null_processor' -require 'ldclient-rb/impl/flag_tracker' require 'ldclient-rb/impl/broadcaster' module LaunchDarkly @@ -51,10 +50,6 @@ def initialize(sdk_key, config) # Set up data source plumbing @data_source_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @config.logger) @flag_change_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @config.logger) - @flag_tracker_impl = LaunchDarkly::Impl::FlagTracker.new( - @flag_change_broadcaster, - lambda { |_key, _context| nil } # Replaced by client to use its evaluation method - ) @data_source_update_sink = LaunchDarkly::Impl::DataSource::UpdateSink.new( @store_wrapper, @data_source_broadcaster, @@ -105,18 +100,6 @@ def store @store_wrapper end - # - # Injects the flag value evaluation function used by the flag tracker to - # compute FlagValueChange events. The function signature should be - # (key, context) -> value. - # - # @param eval_fn [Proc] The evaluation function - # @return [void] - # - def set_flag_value_eval_fn(eval_fn) - @flag_tracker_impl = LaunchDarkly::Impl::FlagTracker.new(@flag_change_broadcaster, eval_fn) - end - # # Sets the diagnostic accumulator for streaming initialization metrics. # This should be called before start() to ensure metrics are collected. @@ -147,12 +130,12 @@ def data_store_status_provider end # - # Returns the flag tracker. + # Returns the broadcaster for flag change notifications. # - # @return [LaunchDarkly::Interfaces::FlagTracker] + # @return [LaunchDarkly::Impl::Broadcaster] # - def flag_tracker - @flag_tracker_impl + def flag_change_broadcaster + @flag_change_broadcaster end # diff --git a/spec/impl/datasystem/fdv1_spec.rb b/spec/impl/datasystem/fdv1_spec.rb index 77b1e876..0d71ce29 100644 --- a/spec/impl/datasystem/fdv1_spec.rb +++ b/spec/impl/datasystem/fdv1_spec.rb @@ -11,22 +11,6 @@ module DataSystem subject { FDv1.new(sdk_key, config) } describe "#initialize" do - it "creates data store status provider" do - expect(subject.data_store_status_provider).to be_a(LaunchDarkly::Impl::DataStore::StatusProvider) - end - - it "creates data source status provider" do - expect(subject.data_source_status_provider).to be_a(LaunchDarkly::Impl::DataSource::StatusProvider) - end - - it "creates flag tracker" do - expect(subject.flag_tracker).to be_a(LaunchDarkly::Impl::FlagTracker) - end - - it "creates store wrapper" do - expect(subject.store).to be_a(LaunchDarkly::Impl::FeatureStoreClientWrapper) - end - it "injects data_source_update_sink into config" do subject # Force creation of FDv1 instance expect(config.data_source_update_sink).to be_a(LaunchDarkly::Impl::DataSource::UpdateSink) @@ -139,20 +123,6 @@ module DataSystem end end - describe "#set_flag_value_eval_fn" do - it "updates the flag tracker with the evaluation function" do - eval_fn = ->(key, context) { "value" } - original_tracker = subject.flag_tracker - - subject.set_flag_value_eval_fn(eval_fn) - - # Should have created a new flag tracker with the eval function - expect(subject.flag_tracker).to be_a(LaunchDarkly::Impl::FlagTracker) - # The tracker should be different (new instance with eval_fn) - expect(subject.flag_tracker.object_id).not_to eq(original_tracker.object_id) - end - end - describe "#set_diagnostic_accumulator" do it "stores the diagnostic accumulator" do diagnostic_accumulator = double("DiagnosticAccumulator") @@ -172,9 +142,9 @@ module DataSystem end end - describe "#flag_tracker" do - it "returns the flag tracker" do - expect(subject.flag_tracker).to be_a(LaunchDarkly::Impl::FlagTracker) + describe "#flag_change_broadcaster" do + it "returns the flag change broadcaster" do + expect(subject.flag_change_broadcaster).to be_a(LaunchDarkly::Impl::Broadcaster) end end diff --git a/spec/impl/datasystem_spec.rb b/spec/impl/datasystem_spec.rb index e23347fe..7411c40f 100644 --- a/spec/impl/datasystem_spec.rb +++ b/spec/impl/datasystem_spec.rb @@ -28,8 +28,8 @@ module Impl expect { test_instance.data_store_status_provider }.to raise_error(NotImplementedError, /must implement #data_store_status_provider/) end - it "flag_tracker raises NotImplementedError" do - expect { test_instance.flag_tracker }.to raise_error(NotImplementedError, /must implement #flag_tracker/) + it "flag_change_broadcaster raises NotImplementedError" do + expect { test_instance.flag_change_broadcaster }.to raise_error(NotImplementedError, /must implement #flag_change_broadcaster/) end it "data_availability raises NotImplementedError" do @@ -44,10 +44,6 @@ module Impl expect { test_instance.store }.to raise_error(NotImplementedError, /must implement #store/) end - it "set_flag_value_eval_fn raises NotImplementedError" do - expect { test_instance.set_flag_value_eval_fn(nil) }.to raise_error(NotImplementedError, /must implement #set_flag_value_eval_fn/) - end - it "set_diagnostic_accumulator raises NotImplementedError" do accumulator = double("DiagnosticAccumulator") expect { test_instance.set_diagnostic_accumulator(accumulator) }.to raise_error(NotImplementedError, /must implement #set_diagnostic_accumulator/) From 8784a7eec9fddbeb867c40f6c4906b254eb8de20 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Wed, 3 Dec 2025 20:47:39 +0000 Subject: [PATCH 04/10] don't create new update processor is already started --- .../impl/datasource/null_processor.rb | 2 -- lib/ldclient-rb/impl/datasystem/fdv1.rb | 13 ++++++----- .../test_data/test_data_source.rb | 1 - spec/impl/datasystem/fdv1_spec.rb | 22 +++++++++++++++++++ 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/lib/ldclient-rb/impl/datasource/null_processor.rb b/lib/ldclient-rb/impl/datasource/null_processor.rb index 39da60d1..14804418 100644 --- a/lib/ldclient-rb/impl/datasource/null_processor.rb +++ b/lib/ldclient-rb/impl/datasource/null_processor.rb @@ -7,8 +7,6 @@ module DataSource # A minimal UpdateProcessor implementation used when the SDK is in offline mode # or daemon (LDD) mode. It does nothing except mark itself as initialized. # - # @private - # class NullUpdateProcessor include LaunchDarkly::Interfaces::DataSource diff --git a/lib/ldclient-rb/impl/datasystem/fdv1.rb b/lib/ldclient-rb/impl/datasystem/fdv1.rb index 6ee88a55..bd617cf8 100644 --- a/lib/ldclient-rb/impl/datasystem/fdv1.rb +++ b/lib/ldclient-rb/impl/datasystem/fdv1.rb @@ -12,8 +12,6 @@ module DataSystem # FDv1 wires the existing v1 data source and store behavior behind the # generic DataSystem surface. # - # @private - # class FDv1 include LaunchDarkly::Impl::DataSystem @@ -63,7 +61,7 @@ def initialize(sdk_key, config) # Ensure v1 processors can find the sink via config for status updates @config.data_source_update_sink = @data_source_update_sink - # Update processor created in start(), because it needs the ready event + # Update processor created in start() @update_processor = nil # Diagnostic accumulator provided by client for streaming metrics @@ -74,15 +72,20 @@ def initialize(sdk_key, config) # Starts the v1 update processor and returns immediately. The returned event # will be set by the processor upon first successful initialization or upon permanent failure. # + # If called multiple times, returns the same event as the first call. The update + # processor is created only once, and subsequent calls delegate to the processor's + # own start method which handles multiple invocations. + # # @return [Concurrent::Event] Event that will be set when initialization is complete # def start - @update_processor = make_update_processor + @update_processor ||= make_update_processor @update_processor.start end # - # Halts the data system, stopping the update processor and shutting down the executor. + # Halts the data system, stopping the update processor and shutting down the executor, + # making the data system no longer usable. # # @return [void] # diff --git a/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb b/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb index a2799a7d..4533e415 100644 --- a/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +++ b/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb @@ -5,7 +5,6 @@ module LaunchDarkly module Impl module Integrations module TestData - # @private class TestDataSource include LaunchDarkly::Interfaces::DataSource diff --git a/spec/impl/datasystem/fdv1_spec.rb b/spec/impl/datasystem/fdv1_spec.rb index 0d71ce29..ae51dcc7 100644 --- a/spec/impl/datasystem/fdv1_spec.rb +++ b/spec/impl/datasystem/fdv1_spec.rb @@ -95,6 +95,28 @@ module DataSystem expect(ready_event).to be_a(Concurrent::Event) end end + + it "returns the same event on multiple calls" do + first_event = subject.start + second_event = subject.start + third_event = subject.start + + expect(second_event).to be(first_event) + expect(third_event).to be(first_event) + end + + it "does not create a new processor on subsequent calls" do + processor = MockUpdateProcessor.new + allow(subject).to receive(:make_update_processor).and_return(processor) + + subject.start + expect(subject).to have_received(:make_update_processor).once + + subject.start + subject.start + # Should still only be called once + expect(subject).to have_received(:make_update_processor).once + end end describe "#stop" do From 2eca9c9873d4340653e7b7f2ba221674caa9b3f2 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Wed, 3 Dec 2025 21:33:23 +0000 Subject: [PATCH 05/10] always return cached for ldd mode --- lib/ldclient-rb/impl/datasystem/fdv1.rb | 9 ++++++--- spec/impl/datasystem/fdv1_spec.rb | 11 +++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/ldclient-rb/impl/datasystem/fdv1.rb b/lib/ldclient-rb/impl/datasystem/fdv1.rb index bd617cf8..be47b911 100644 --- a/lib/ldclient-rb/impl/datasystem/fdv1.rb +++ b/lib/ldclient-rb/impl/datasystem/fdv1.rb @@ -151,10 +151,13 @@ def flag_change_broadcaster def data_availability return DataAvailability::DEFAULTS if @config.offline? - unless @config.use_ldd? - return DataAvailability::REFRESHED if @update_processor && @update_processor.initialized? - end + # In LDD mode, always return CACHED for backwards compatibility. + # Even though the store might be empty (technically DEFAULTS), we maintain + # the existing behavior where LDD mode is assumed to have data available + # from the external daemon, regardless of the store's initialization state. + return DataAvailability::CACHED if @config.use_ldd? + return DataAvailability::REFRESHED if @update_processor && @update_processor.initialized? return DataAvailability::CACHED if @store_wrapper.initialized? DataAvailability::DEFAULTS diff --git a/spec/impl/datasystem/fdv1_spec.rb b/spec/impl/datasystem/fdv1_spec.rb index ae51dcc7..f651c190 100644 --- a/spec/impl/datasystem/fdv1_spec.rb +++ b/spec/impl/datasystem/fdv1_spec.rb @@ -208,13 +208,12 @@ module DataSystem context "in LDD mode" do let(:config) { LaunchDarkly::Config.new(use_ldd: true) } - it "returns DEFAULTS when store is empty" do - subject.start - expect(subject.data_availability).to eq(DataAvailability::DEFAULTS) - end - - it "returns CACHED when store is initialized" do + it "always returns CACHED for backwards compatibility" do subject.start + # Returns CACHED even when store is empty + expect(subject.data_availability).to eq(DataAvailability::CACHED) + + # Still returns CACHED when store is initialized subject.store.init({}) expect(subject.data_availability).to eq(DataAvailability::CACHED) end From e89b7fba3db1df09da355c447b318fe96265fcc2 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Wed, 3 Dec 2025 21:33:54 +0000 Subject: [PATCH 06/10] fix lint --- spec/impl/datasystem/fdv1_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/impl/datasystem/fdv1_spec.rb b/spec/impl/datasystem/fdv1_spec.rb index f651c190..c51a8305 100644 --- a/spec/impl/datasystem/fdv1_spec.rb +++ b/spec/impl/datasystem/fdv1_spec.rb @@ -212,7 +212,7 @@ module DataSystem subject.start # Returns CACHED even when store is empty expect(subject.data_availability).to eq(DataAvailability::CACHED) - + # Still returns CACHED when store is initialized subject.store.init({}) expect(subject.data_availability).to eq(DataAvailability::CACHED) From 8e84bd15dc2465e2054a099d508667053f971e2c Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Wed, 3 Dec 2025 21:51:42 +0000 Subject: [PATCH 07/10] fix missing require in null update processor --- .../impl/datasource/null_processor.rb | 1 + spec/impl/datasource/null_processor_spec.rb | 57 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 spec/impl/datasource/null_processor_spec.rb diff --git a/lib/ldclient-rb/impl/datasource/null_processor.rb b/lib/ldclient-rb/impl/datasource/null_processor.rb index 14804418..0929e328 100644 --- a/lib/ldclient-rb/impl/datasource/null_processor.rb +++ b/lib/ldclient-rb/impl/datasource/null_processor.rb @@ -1,4 +1,5 @@ require 'concurrent' +require 'ldclient-rb/interfaces' module LaunchDarkly module Impl diff --git a/spec/impl/datasource/null_processor_spec.rb b/spec/impl/datasource/null_processor_spec.rb new file mode 100644 index 00000000..32ee2eaa --- /dev/null +++ b/spec/impl/datasource/null_processor_spec.rb @@ -0,0 +1,57 @@ +require "spec_helper" +require "ldclient-rb/impl/datasource/null_processor" + +module LaunchDarkly + module Impl + module DataSource + describe NullUpdateProcessor do + subject { NullUpdateProcessor.new } + + describe "#initialize" do + it "creates a ready event" do + expect(subject.instance_variable_get(:@ready)).to be_a(Concurrent::Event) + end + end + + describe "#start" do + it "returns a ready event that is already set" do + ready_event = subject.start + expect(ready_event).to be_a(Concurrent::Event) + expect(ready_event.set?).to be true + end + + it "returns the same event on multiple calls" do + first_event = subject.start + second_event = subject.start + + expect(second_event).to be(first_event) + end + end + + describe "#stop" do + it "does nothing and does not raise an error" do + expect { subject.stop }.not_to raise_error + end + end + + describe "#initialized?" do + it "always returns true" do + expect(subject.initialized?).to be true + end + + it "returns true even before start is called" do + processor = NullUpdateProcessor.new + expect(processor.initialized?).to be true + end + end + + describe "DataSource interface" do + it "includes the DataSource module" do + expect(subject.class.ancestors).to include(LaunchDarkly::Interfaces::DataSource) + end + end + end + end + end +end + From 421e8574b14ba4f04c16cf518f600d4dc4d54be4 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Wed, 3 Dec 2025 22:54:25 +0000 Subject: [PATCH 08/10] clean up and link docs --- lib/ldclient-rb/impl/datasystem.rb | 10 ++-- lib/ldclient-rb/impl/datasystem/fdv1.rb | 65 +++++-------------------- 2 files changed, 20 insertions(+), 55 deletions(-) diff --git a/lib/ldclient-rb/impl/datasystem.rb b/lib/ldclient-rb/impl/datasystem.rb index 4dbf8169..53f434a1 100644 --- a/lib/ldclient-rb/impl/datasystem.rb +++ b/lib/ldclient-rb/impl/datasystem.rb @@ -22,6 +22,8 @@ module DataSystem # This method will return immediately. The returned event will be set when the system # has reached an initial state (either permanently failed, e.g. due to bad auth, or succeeded). # + # If called multiple times, returns the same event as the first call. + # # @return [Concurrent::Event] Event that will be set when initialization is complete # def start @@ -30,7 +32,7 @@ def start # # Halts the data system. Should be called when the client is closed to stop any long running - # operations. + # operations. Makes the data system no longer usable. # # @return [void] # @@ -80,7 +82,9 @@ def flag_change_broadcaster # # Indicates what form of data is currently available. # - # @return [Symbol] One of DataAvailability constants + # This is calculated dynamically based on current system state. + # + # @return [Symbol] one of the {DataAvailability} constants # def data_availability raise NotImplementedError, "#{self.class} must implement #data_availability" @@ -89,7 +93,7 @@ def data_availability # # Indicates the ideal form of data attainable given the current configuration. # - # @return [Symbol] One of DataAvailability constants + # @return [Symbol] one of the {#DataAvailability} constants # def target_availability raise NotImplementedError, "#{self.class} must implement #target_availability" diff --git a/lib/ldclient-rb/impl/datasystem/fdv1.rb b/lib/ldclient-rb/impl/datasystem/fdv1.rb index be47b911..64c60ae6 100644 --- a/lib/ldclient-rb/impl/datasystem/fdv1.rb +++ b/lib/ldclient-rb/impl/datasystem/fdv1.rb @@ -12,6 +12,8 @@ module DataSystem # FDv1 wires the existing v1 data source and store behavior behind the # generic DataSystem surface. # + # @see DataSystem + # class FDv1 include LaunchDarkly::Impl::DataSystem @@ -68,85 +70,48 @@ def initialize(sdk_key, config) @diagnostic_accumulator = nil end - # - # Starts the v1 update processor and returns immediately. The returned event - # will be set by the processor upon first successful initialization or upon permanent failure. - # - # If called multiple times, returns the same event as the first call. The update - # processor is created only once, and subsequent calls delegate to the processor's - # own start method which handles multiple invocations. - # - # @return [Concurrent::Event] Event that will be set when initialization is complete - # + # (see DataSystem#start) def start @update_processor ||= make_update_processor @update_processor.start end - # - # Halts the data system, stopping the update processor and shutting down the executor, - # making the data system no longer usable. - # - # @return [void] - # + # (see DataSystem#stop) def stop @update_processor&.stop @shared_executor.shutdown end - # - # Returns the feature store wrapper used by this data system. - # - # @return [LaunchDarkly::Impl::DataStore::ClientWrapper] - # + # (see DataSystem#store) def store @store_wrapper end - # - # Sets the diagnostic accumulator for streaming initialization metrics. - # This should be called before start() to ensure metrics are collected. - # - # @param diagnostic_accumulator [DiagnosticAccumulator] The diagnostic accumulator - # @return [void] - # + # (see DataSystem#set_diagnostic_accumulator) def set_diagnostic_accumulator(diagnostic_accumulator) @diagnostic_accumulator = diagnostic_accumulator end - # - # Returns the data source status provider. - # - # @return [LaunchDarkly::Interfaces::DataSource::StatusProvider] - # + # (see DataSystem#data_source_status_provider) def data_source_status_provider @data_source_status_provider end - # - # Returns the data store status provider. - # - # @return [LaunchDarkly::Interfaces::DataStore::StatusProvider] - # + # (see DataSystem#data_store_status_provider) def data_store_status_provider @data_store_status_provider end - # - # Returns the broadcaster for flag change notifications. - # - # @return [LaunchDarkly::Impl::Broadcaster] - # + # (see DataSystem#flag_change_broadcaster) def flag_change_broadcaster @flag_change_broadcaster end # - # Indicates what form of data is currently available. - # - # This is calculated dynamically based on current system state. + # (see DataSystem#data_availability) # - # @return [Symbol] One of DataAvailability constants + # In LDD mode, always returns CACHED for backwards compatibility, + # even if the store is empty. # def data_availability return DataAvailability::DEFAULTS if @config.offline? @@ -163,11 +128,7 @@ def data_availability DataAvailability::DEFAULTS end - # - # Indicates the ideal form of data attainable given the current configuration. - # - # @return [Symbol] One of DataAvailability constants - # + # (see DataSystem#target_availability) def target_availability return DataAvailability::DEFAULTS if @config.offline? return DataAvailability::CACHED if @config.use_ldd? From 67a6c0f486aa115df05fb010b5700f5c0acd2b8d Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Wed, 3 Dec 2025 23:09:51 +0000 Subject: [PATCH 09/10] use ruby file and folder naming conventions --- .../impl/{datasource => data_source}/null_processor.rb | 0 lib/ldclient-rb/impl/{datasystem.rb => data_system.rb} | 0 lib/ldclient-rb/impl/{datasystem => data_system}/fdv1.rb | 7 ++++--- lib/ldclient-rb/ldclient.rb | 2 +- .../{datasource => data_source}/null_processor_spec.rb | 2 +- spec/impl/{datasystem => data_system}/fdv1_spec.rb | 2 +- spec/impl/datasystem_spec.rb | 2 +- spec/mock_components.rb | 2 +- 8 files changed, 9 insertions(+), 8 deletions(-) rename lib/ldclient-rb/impl/{datasource => data_source}/null_processor.rb (100%) rename lib/ldclient-rb/impl/{datasystem.rb => data_system.rb} (100%) rename lib/ldclient-rb/impl/{datasystem => data_system}/fdv1.rb (97%) rename spec/impl/{datasource => data_source}/null_processor_spec.rb (96%) rename spec/impl/{datasystem => data_system}/fdv1_spec.rb (99%) diff --git a/lib/ldclient-rb/impl/datasource/null_processor.rb b/lib/ldclient-rb/impl/data_source/null_processor.rb similarity index 100% rename from lib/ldclient-rb/impl/datasource/null_processor.rb rename to lib/ldclient-rb/impl/data_source/null_processor.rb diff --git a/lib/ldclient-rb/impl/datasystem.rb b/lib/ldclient-rb/impl/data_system.rb similarity index 100% rename from lib/ldclient-rb/impl/datasystem.rb rename to lib/ldclient-rb/impl/data_system.rb diff --git a/lib/ldclient-rb/impl/datasystem/fdv1.rb b/lib/ldclient-rb/impl/data_system/fdv1.rb similarity index 97% rename from lib/ldclient-rb/impl/datasystem/fdv1.rb rename to lib/ldclient-rb/impl/data_system/fdv1.rb index 64c60ae6..177ef7df 100644 --- a/lib/ldclient-rb/impl/datasystem/fdv1.rb +++ b/lib/ldclient-rb/impl/data_system/fdv1.rb @@ -1,9 +1,10 @@ require 'concurrent' -require 'ldclient-rb/impl/datasystem' +require 'ldclient-rb/impl/broadcaster' require 'ldclient-rb/impl/data_source' +require 'ldclient-rb/impl/data_source/null_processor' require 'ldclient-rb/impl/data_store' -require 'ldclient-rb/impl/datasource/null_processor' -require 'ldclient-rb/impl/broadcaster' +require 'ldclient-rb/impl/data_system' +require 'ldclient-rb/impl/store_client_wrapper' module LaunchDarkly module Impl diff --git a/lib/ldclient-rb/ldclient.rb b/lib/ldclient-rb/ldclient.rb index f0dc852b..16f8a442 100644 --- a/lib/ldclient-rb/ldclient.rb +++ b/lib/ldclient-rb/ldclient.rb @@ -2,7 +2,7 @@ require "ldclient-rb/impl/broadcaster" require "ldclient-rb/impl/data_source" require "ldclient-rb/impl/data_store" -require "ldclient-rb/impl/datasource/null_processor" +require "ldclient-rb/impl/data_source/null_processor" require "ldclient-rb/impl/diagnostic_events" require "ldclient-rb/impl/evaluator" require "ldclient-rb/impl/evaluation_with_hook_result" diff --git a/spec/impl/datasource/null_processor_spec.rb b/spec/impl/data_source/null_processor_spec.rb similarity index 96% rename from spec/impl/datasource/null_processor_spec.rb rename to spec/impl/data_source/null_processor_spec.rb index 32ee2eaa..3249c61f 100644 --- a/spec/impl/datasource/null_processor_spec.rb +++ b/spec/impl/data_source/null_processor_spec.rb @@ -1,5 +1,5 @@ require "spec_helper" -require "ldclient-rb/impl/datasource/null_processor" +require "ldclient-rb/impl/data_source/null_processor" module LaunchDarkly module Impl diff --git a/spec/impl/datasystem/fdv1_spec.rb b/spec/impl/data_system/fdv1_spec.rb similarity index 99% rename from spec/impl/datasystem/fdv1_spec.rb rename to spec/impl/data_system/fdv1_spec.rb index c51a8305..6411e2a3 100644 --- a/spec/impl/datasystem/fdv1_spec.rb +++ b/spec/impl/data_system/fdv1_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" require "mock_components" -require "ldclient-rb/impl/datasystem/fdv1" +require "ldclient-rb/impl/data_system/fdv1" module LaunchDarkly module Impl diff --git a/spec/impl/datasystem_spec.rb b/spec/impl/datasystem_spec.rb index 7411c40f..7805900b 100644 --- a/spec/impl/datasystem_spec.rb +++ b/spec/impl/datasystem_spec.rb @@ -1,5 +1,5 @@ require "spec_helper" -require "ldclient-rb/impl/datasystem" +require "ldclient-rb/impl/data_system" module LaunchDarkly module Impl diff --git a/spec/mock_components.rb b/spec/mock_components.rb index 7e7533ee..c7dc8b9b 100644 --- a/spec/mock_components.rb +++ b/spec/mock_components.rb @@ -2,7 +2,7 @@ require "ldclient-rb/impl/big_segments" require "ldclient-rb/impl/evaluator" -require "ldclient-rb/impl/datasource/null_processor" +require "ldclient-rb/impl/data_source/null_processor" require "ldclient-rb/interfaces" def sdk_key From 45e8c70c8f9aeedfd0e9c2c00500129f93225518 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Thu, 4 Dec 2025 15:09:00 +0000 Subject: [PATCH 10/10] return refreshed for backwards compatibility if ldd mode --- lib/ldclient-rb/impl/data_system/fdv1.rb | 8 -------- spec/impl/data_system/fdv1_spec.rb | 14 +++++++------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/lib/ldclient-rb/impl/data_system/fdv1.rb b/lib/ldclient-rb/impl/data_system/fdv1.rb index 177ef7df..e5e20062 100644 --- a/lib/ldclient-rb/impl/data_system/fdv1.rb +++ b/lib/ldclient-rb/impl/data_system/fdv1.rb @@ -116,13 +116,6 @@ def flag_change_broadcaster # def data_availability return DataAvailability::DEFAULTS if @config.offline? - - # In LDD mode, always return CACHED for backwards compatibility. - # Even though the store might be empty (technically DEFAULTS), we maintain - # the existing behavior where LDD mode is assumed to have data available - # from the external daemon, regardless of the store's initialization state. - return DataAvailability::CACHED if @config.use_ldd? - return DataAvailability::REFRESHED if @update_processor && @update_processor.initialized? return DataAvailability::CACHED if @store_wrapper.initialized? @@ -132,7 +125,6 @@ def data_availability # (see DataSystem#target_availability) def target_availability return DataAvailability::DEFAULTS if @config.offline? - return DataAvailability::CACHED if @config.use_ldd? DataAvailability::REFRESHED end diff --git a/spec/impl/data_system/fdv1_spec.rb b/spec/impl/data_system/fdv1_spec.rb index 6411e2a3..9c905a2f 100644 --- a/spec/impl/data_system/fdv1_spec.rb +++ b/spec/impl/data_system/fdv1_spec.rb @@ -208,14 +208,14 @@ module DataSystem context "in LDD mode" do let(:config) { LaunchDarkly::Config.new(use_ldd: true) } - it "always returns CACHED for backwards compatibility" do + it "always returns REFRESHED for backwards compatibility" do subject.start - # Returns CACHED even when store is empty - expect(subject.data_availability).to eq(DataAvailability::CACHED) + # Returns REFRESHED even when store is empty + expect(subject.data_availability).to eq(DataAvailability::REFRESHED) - # Still returns CACHED when store is initialized + # Still returns REFRESHED when store is initialized subject.store.init({}) - expect(subject.data_availability).to eq(DataAvailability::CACHED) + expect(subject.data_availability).to eq(DataAvailability::REFRESHED) end end end @@ -238,8 +238,8 @@ module DataSystem context "with LDD mode" do let(:config) { LaunchDarkly::Config.new(use_ldd: true) } - it "returns CACHED" do - expect(subject.target_availability).to eq(DataAvailability::CACHED) + it "returns REFRESHED" do + expect(subject.target_availability).to eq(DataAvailability::REFRESHED) end end end