diff --git a/ldclient/impl/datasystem/fdv1.py b/ldclient/impl/datasystem/fdv1.py index 07828a5..23b76ad 100644 --- a/ldclient/impl/datasystem/fdv1.py +++ b/ldclient/impl/datasystem/fdv1.py @@ -134,6 +134,11 @@ def data_availability(self) -> DataAvailability: if self._config.offline: return DataAvailability.DEFAULTS + if self._config.use_ldd: + return DataAvailability.CACHED \ + if self._store_wrapper.initialized \ + else DataAvailability.DEFAULTS + if self._update_processor is not None and self._update_processor.initialized(): return DataAvailability.REFRESHED @@ -146,7 +151,9 @@ def data_availability(self) -> DataAvailability: def target_availability(self) -> DataAvailability: if self._config.offline: return DataAvailability.DEFAULTS - # In LDD mode or normal connected modes, the ideal is to be refreshed + if self._config.use_ldd: + return DataAvailability.CACHED + return DataAvailability.REFRESHED def _make_update_processor(self, config: Config, store: FeatureStore, ready: Event): diff --git a/ldclient/impl/datasystem/fdv2.py b/ldclient/impl/datasystem/fdv2.py index d411fd5..9383d65 100644 --- a/ldclient/impl/datasystem/fdv2.py +++ b/ldclient/impl/datasystem/fdv2.py @@ -267,7 +267,6 @@ def __init__( self._primary_synchronizer_builder: Optional[Builder[Synchronizer]] = data_system_config.primary_synchronizer self._secondary_synchronizer_builder = data_system_config.secondary_synchronizer self._fdv1_fallback_synchronizer_builder = data_system_config.fdv1_fallback_synchronizer - self._disabled = self._config.offline # Diagnostic accumulator provided by client for streaming metrics self._diagnostic_accumulator: Optional[DiagnosticAccumulator] = None @@ -319,7 +318,7 @@ def start(self, set_on_ready: Event): :param set_on_ready: Event to set when the system is ready or has failed """ - if self._disabled: + if self._config.offline: log.warning("Data system is disabled, SDK will return application-defined default values") set_on_ready.set() return @@ -688,7 +687,7 @@ def data_availability(self) -> DataAvailability: if self._store.selector().is_defined(): return DataAvailability.REFRESHED - if not self._configured_with_data_sources or self._store.is_initialized(): + if self._store.is_initialized(): return DataAvailability.CACHED return DataAvailability.DEFAULTS @@ -696,7 +695,13 @@ def data_availability(self) -> DataAvailability: @property def target_availability(self) -> DataAvailability: """Get the target data availability level based on configuration.""" + if self._config.offline: + return DataAvailability.DEFAULTS + if self._configured_with_data_sources: return DataAvailability.REFRESHED + if self._data_system_config.data_store is None: + return DataAvailability.DEFAULTS + return DataAvailability.CACHED diff --git a/ldclient/testing/impl/datasystem/test_fdv1_availability.py b/ldclient/testing/impl/datasystem/test_fdv1_availability.py new file mode 100644 index 0000000..622cc94 --- /dev/null +++ b/ldclient/testing/impl/datasystem/test_fdv1_availability.py @@ -0,0 +1,68 @@ +# pylint: disable=missing-docstring + +from threading import Event + +from ldclient.config import Config +from ldclient.feature_store import InMemoryFeatureStore +from ldclient.impl.datasystem import DataAvailability +from ldclient.impl.datasystem.fdv1 import FDv1 +from ldclient.versioned_data_kind import FEATURES + + +def test_fdv1_availability_offline(): + """Test that FDv1 returns DEFAULTS for both data and target availability when offline.""" + config = Config(sdk_key="sdk-key", offline=True) + fdv1 = FDv1(config) + + assert fdv1.data_availability == DataAvailability.DEFAULTS + assert fdv1.target_availability == DataAvailability.DEFAULTS + + +def test_fdv1_availability_ldd_mode_uninitialized(): + """Test that FDv1 returns DEFAULTS for data and CACHED for target when LDD mode with uninitialized store.""" + store = InMemoryFeatureStore() + config = Config(sdk_key="sdk-key", use_ldd=True, feature_store=store) + fdv1 = FDv1(config) + + # Store is not initialized yet + assert not store.initialized + assert fdv1.data_availability == DataAvailability.DEFAULTS + assert fdv1.target_availability == DataAvailability.CACHED + + +def test_fdv1_availability_ldd_mode_initialized(): + """Test that FDv1 returns CACHED for both when LDD mode with initialized store.""" + store = InMemoryFeatureStore() + config = Config(sdk_key="sdk-key", use_ldd=True, feature_store=store) + fdv1 = FDv1(config) + + # Initialize the store + store.init({FEATURES: {}}) + + assert store.initialized + assert fdv1.data_availability == DataAvailability.CACHED + assert fdv1.target_availability == DataAvailability.CACHED + + +def test_fdv1_availability_normal_mode_uninitialized(): + """Test that FDv1 returns DEFAULTS for data and REFRESHED for target in normal mode when not initialized.""" + store = InMemoryFeatureStore() + config = Config(sdk_key="sdk-key", feature_store=store) + fdv1 = FDv1(config) + + # Update processor not started, store not initialized + assert fdv1.data_availability == DataAvailability.DEFAULTS + assert fdv1.target_availability == DataAvailability.REFRESHED + + +def test_fdv1_availability_normal_mode_store_initialized(): + """Test that FDv1 returns CACHED for data and REFRESHED for target when store is initialized but update processor is not.""" + store = InMemoryFeatureStore() + config = Config(sdk_key="sdk-key", feature_store=store) + fdv1 = FDv1(config) + + # Initialize store but don't start update processor + fdv1._store_wrapper.init({FEATURES: {}}) + + assert fdv1.data_availability == DataAvailability.CACHED + assert fdv1.target_availability == DataAvailability.REFRESHED diff --git a/ldclient/testing/impl/datasystem/test_fdv2_datasystem.py b/ldclient/testing/impl/datasystem/test_fdv2_datasystem.py index c77f799..62298cd 100644 --- a/ldclient/testing/impl/datasystem/test_fdv2_datasystem.py +++ b/ldclient/testing/impl/datasystem/test_fdv2_datasystem.py @@ -545,3 +545,157 @@ def listener(_: FlagChange): fdv2.stop() finally: os.remove(path) + + +def test_fdv2_availability_offline(): + """Test that FDv2 returns DEFAULTS for target availability and data availability when offline.""" + data_system_config = DataSystemConfig( + initializers=None, + primary_synchronizer=None, + ) + + fdv2 = FDv2(Config(sdk_key="dummy", offline=True), data_system_config) + + assert fdv2.data_availability == DataAvailability.DEFAULTS + assert fdv2.target_availability == DataAvailability.DEFAULTS + + +def test_fdv2_availability_with_data_sources_no_store(): + """Test that FDv2 returns DEFAULTS for data and REFRESHED for target when configured with data sources but no store and uninitialized.""" + td = TestDataV2.data_source() + + data_system_config = DataSystemConfig( + initializers=None, + primary_synchronizer=td.build_synchronizer, + ) + + fdv2 = FDv2(Config(sdk_key="dummy"), data_system_config) + + # Store is not initialized, and we have data sources configured + assert not fdv2._store.is_initialized() + assert fdv2.data_availability == DataAvailability.DEFAULTS + assert fdv2.target_availability == DataAvailability.REFRESHED + + +def test_fdv2_availability_no_data_sources_with_readonly_store_uninitialized(): + """Test that FDv2 returns DEFAULTS for both when no data sources and read-only store is uninitialized.""" + from ldclient.interfaces import DataStoreMode + from ldclient.testing.impl.datasystem.test_fdv2_persistence import ( + StubFeatureStore + ) + + store = StubFeatureStore() + data_system_config = DataSystemConfig( + initializers=None, + primary_synchronizer=None, + data_store=store, + data_store_mode=DataStoreMode.READ_ONLY, + ) + + fdv2 = FDv2(Config(sdk_key="dummy"), data_system_config) + + # Store is not initialized + assert not store.initialized + assert fdv2.data_availability == DataAvailability.DEFAULTS + assert fdv2.target_availability == DataAvailability.CACHED + + +def test_fdv2_availability_no_data_sources_with_readonly_store_initialized(): + """Test that FDv2 returns CACHED for both when no data sources and read-only store is initialized.""" + from ldclient.interfaces import DataStoreMode + from ldclient.testing.impl.datasystem.test_fdv2_persistence import ( + StubFeatureStore + ) + + store = StubFeatureStore() + store.init({FEATURES: {}}) + + data_system_config = DataSystemConfig( + initializers=None, + primary_synchronizer=None, + data_store=store, + data_store_mode=DataStoreMode.READ_ONLY, + ) + + fdv2 = FDv2(Config(sdk_key="dummy"), data_system_config) + + # Store is initialized + assert store.initialized + assert fdv2.data_availability == DataAvailability.CACHED + assert fdv2.target_availability == DataAvailability.CACHED + + +def test_fdv2_availability_no_data_sources_with_readwrite_store_initialized(): + """Test that FDv2 returns CACHED for both when no data sources and read-write store is initialized.""" + from ldclient.interfaces import DataStoreMode + from ldclient.testing.impl.datasystem.test_fdv2_persistence import ( + StubFeatureStore + ) + + store = StubFeatureStore() + store.init({FEATURES: {}}) + + data_system_config = DataSystemConfig( + initializers=None, + primary_synchronizer=None, + data_store=store, + data_store_mode=DataStoreMode.READ_WRITE, + ) + + fdv2 = FDv2(Config(sdk_key="dummy"), data_system_config) + + # Store is initialized + assert store.initialized + assert fdv2.data_availability == DataAvailability.CACHED + assert fdv2.target_availability == DataAvailability.CACHED + + +def test_fdv2_availability_with_data_sources_and_store_uninitialized(): + """Test that FDv2 returns DEFAULTS for data and REFRESHED for target when data sources configured with uninitialized store.""" + from ldclient.interfaces import DataStoreMode + from ldclient.testing.impl.datasystem.test_fdv2_persistence import ( + StubFeatureStore + ) + + td = TestDataV2.data_source() + store = StubFeatureStore() + + data_system_config = DataSystemConfig( + initializers=None, + primary_synchronizer=td.build_synchronizer, + data_store=store, + data_store_mode=DataStoreMode.READ_WRITE, + ) + + fdv2 = FDv2(Config(sdk_key="dummy"), data_system_config) + + # Store is not initialized + assert not store.initialized + assert fdv2.data_availability == DataAvailability.DEFAULTS + assert fdv2.target_availability == DataAvailability.REFRESHED + + +def test_fdv2_availability_with_data_sources_and_store_initialized(): + """Test that FDv2 returns CACHED for data and REFRESHED for target when data sources configured with initialized store.""" + from ldclient.interfaces import DataStoreMode + from ldclient.testing.impl.datasystem.test_fdv2_persistence import ( + StubFeatureStore + ) + + td = TestDataV2.data_source() + store = StubFeatureStore() + store.init({FEATURES: {}}) + + data_system_config = DataSystemConfig( + initializers=None, + primary_synchronizer=td.build_synchronizer, + data_store=store, + data_store_mode=DataStoreMode.READ_WRITE, + ) + + fdv2 = FDv2(Config(sdk_key="dummy"), data_system_config) + + # Store is initialized but selector not defined yet (synchronizer not started) + assert store.initialized + assert fdv2.data_availability == DataAvailability.CACHED + assert fdv2.target_availability == DataAvailability.REFRESHED