Skip to content
Draft
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
9 changes: 8 additions & 1 deletion ldclient/impl/datasystem/fdv1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Expand Down
11 changes: 8 additions & 3 deletions ldclient/impl/datasystem/fdv2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -688,15 +687,21 @@ 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():
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Missing offline check in FDv2 data_availability property

The data_availability property in FDv2 is missing an offline check, unlike target_availability which explicitly checks self._config.offline and returns DEFAULTS. In FDv1, both properties check for offline mode first. Without this check, if the store happens to be initialized while in offline mode, data_availability would return CACHED instead of DEFAULTS, creating inconsistent behavior between the two availability properties and between FDv1 and FDv2.

Fix in Cursor Fix in Web

return DataAvailability.CACHED

return DataAvailability.DEFAULTS

@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
68 changes: 68 additions & 0 deletions ldclient/testing/impl/datasystem/test_fdv1_availability.py
Original file line number Diff line number Diff line change
@@ -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
154 changes: 154 additions & 0 deletions ldclient/testing/impl/datasystem/test_fdv2_datasystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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