Skip to content

Commit 8dd4c89

Browse files
authored
chore: Add persistence store support for FDv2 (#357)
1 parent 4b6a168 commit 8dd4c89

File tree

8 files changed

+820
-94
lines changed

8 files changed

+820
-94
lines changed

ldclient/config.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from ldclient.interfaces import (
2020
BigSegmentStore,
2121
DataSourceUpdateSink,
22+
DataStoreMode,
2223
EventProcessor,
2324
FeatureStore,
2425
UpdateProcessor
@@ -161,19 +162,23 @@ def disable_ssl_verification(self) -> bool:
161162

162163
@dataclass(frozen=True)
163164
class DataSystemConfig:
164-
"""
165-
Configuration for LaunchDarkly's data acquisition strategy.
166-
"""
165+
"""Configuration for LaunchDarkly's data acquisition strategy."""
167166

168167
initializers: Optional[List[Builder[Initializer]]]
169168
"""The initializers for the data system."""
170169

171-
primary_synchronizer: Builder[Synchronizer]
170+
primary_synchronizer: Optional[Builder[Synchronizer]]
172171
"""The primary synchronizer for the data system."""
173172

174173
secondary_synchronizer: Optional[Builder[Synchronizer]] = None
175174
"""The secondary synchronizers for the data system."""
176175

176+
data_store_mode: DataStoreMode = DataStoreMode.READ_WRITE
177+
"""The data store mode specifies the mode in which the persistent store will operate, if present."""
178+
179+
data_store: Optional[FeatureStore] = None
180+
"""The (optional) persistent data store instance."""
181+
177182
# TODO(fdv2): Implement this synchronizer up and hook it up everywhere.
178183
# TODO(fdv2): Remove this when FDv2 is fully launched
179184
fdv1_fallback_synchronizer: Optional[Builder[Synchronizer]] = None

ldclient/impl/datasourcev2/status.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import time
2+
from copy import copy
23
from typing import Callable, Optional
34

5+
from ldclient.impl.datasystem.store import Store
46
from ldclient.impl.listeners import Listeners
57
from ldclient.impl.rwlock import ReadWriteLock
68
from ldclient.interfaces import (
79
DataSourceErrorInfo,
810
DataSourceState,
911
DataSourceStatus,
10-
DataSourceStatusProvider
12+
DataSourceStatusProvider,
13+
DataStoreStatus,
14+
DataStoreStatusProvider,
15+
FeatureStore
1116
)
1217

1318

@@ -55,3 +60,50 @@ def add_listener(self, listener: Callable[[DataSourceStatus], None]):
5560

5661
def remove_listener(self, listener: Callable[[DataSourceStatus], None]):
5762
self.__listeners.remove(listener)
63+
64+
65+
class DataStoreStatusProviderImpl(DataStoreStatusProvider):
66+
def __init__(self, store: Optional[FeatureStore], listeners: Listeners):
67+
self.__store = store
68+
self.__listeners = listeners
69+
70+
self.__lock = ReadWriteLock()
71+
self.__status = DataStoreStatus(True, False)
72+
73+
def update_status(self, status: DataStoreStatus):
74+
"""
75+
update_status is called from the data store to push a status update.
76+
"""
77+
self.__lock.lock()
78+
modified = False
79+
80+
if self.__status != status:
81+
self.__status = status
82+
modified = True
83+
84+
self.__lock.unlock()
85+
86+
if modified:
87+
self.__listeners.notify(status)
88+
89+
@property
90+
def status(self) -> DataStoreStatus:
91+
self.__lock.rlock()
92+
status = copy(self.__status)
93+
self.__lock.runlock()
94+
95+
return status
96+
97+
def is_monitoring_enabled(self) -> bool:
98+
if self.__store is None:
99+
return False
100+
if hasattr(self.__store, "is_monitoring_enabled") is False:
101+
return False
102+
103+
return self.__store.is_monitoring_enabled() # type: ignore
104+
105+
def add_listener(self, listener: Callable[[DataStoreStatus], None]):
106+
self.__listeners.add(listener)
107+
108+
def remove_listener(self, listener: Callable[[DataStoreStatus], None]):
109+
self.__listeners.remove(listener)

ldclient/impl/datasystem/config.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
StreamingDataSourceBuilder
1717
)
1818
from ldclient.impl.datasystem import Initializer, Synchronizer
19+
from ldclient.interfaces import DataStoreMode, FeatureStore
1920

2021
T = TypeVar("T")
2122

@@ -30,6 +31,8 @@ class ConfigBuilder: # pylint: disable=too-few-public-methods
3031
_initializers: Optional[List[Builder[Initializer]]] = None
3132
_primary_synchronizer: Optional[Builder[Synchronizer]] = None
3233
_secondary_synchronizer: Optional[Builder[Synchronizer]] = None
34+
_store_mode: DataStoreMode = DataStoreMode.READ_ONLY
35+
_data_store: Optional[FeatureStore] = None
3336

3437
def initializers(self, initializers: Optional[List[Builder[Initializer]]]) -> "ConfigBuilder":
3538
"""
@@ -50,17 +53,27 @@ def synchronizers(
5053
self._secondary_synchronizer = secondary
5154
return self
5255

56+
def data_store(self, data_store: FeatureStore, store_mode: DataStoreMode) -> "ConfigBuilder":
57+
"""
58+
Sets the data store configuration for the data system.
59+
"""
60+
self._data_store = data_store
61+
self._store_mode = store_mode
62+
return self
63+
5364
def build(self) -> DataSystemConfig:
5465
"""
5566
Builds the data system configuration.
5667
"""
57-
if self._primary_synchronizer is None:
58-
raise ValueError("Primary synchronizer must be set")
68+
if self._secondary_synchronizer is not None and self._primary_synchronizer is None:
69+
raise ValueError("Primary synchronizer must be set if secondary is set")
5970

6071
return DataSystemConfig(
6172
initializers=self._initializers,
6273
primary_synchronizer=self._primary_synchronizer,
6374
secondary_synchronizer=self._secondary_synchronizer,
75+
data_store_mode=self._store_mode,
76+
data_store=self._data_store,
6477
)
6578

6679

@@ -147,18 +160,29 @@ def custom() -> ConfigBuilder:
147160
return ConfigBuilder()
148161

149162

150-
# TODO(fdv2): Implement these methods
151-
#
152-
# Daemon configures the SDK to read from a persistent store integration
153-
# that is populated by Relay Proxy or other SDKs. The SDK will not connect
154-
# to LaunchDarkly. In this mode, the SDK never writes to the data store.
163+
# TODO(fdv2): Need to update these so they don't rely on the LDConfig
164+
def daemon(config: LDConfig, store: FeatureStore) -> ConfigBuilder:
165+
"""
166+
Daemon configures the SDK to read from a persistent store integration
167+
that is populated by Relay Proxy or other SDKs. The SDK will not connect
168+
to LaunchDarkly. In this mode, the SDK never writes to the data store.
169+
"""
170+
return default(config).data_store(store, DataStoreMode.READ_ONLY)
171+
155172

156-
# PersistentStore is similar to Default, with the addition of a persistent
157-
# store integration. Before data has arrived from LaunchDarkly, the SDK is
158-
# able to evaluate flags using data from the persistent store. Once fresh
159-
# data is available, the SDK will no longer read from the persistent store,
160-
# although it will keep it up-to-date.
173+
def persistent_store(config: LDConfig, store: FeatureStore) -> ConfigBuilder:
174+
"""
175+
PersistentStore is similar to Default, with the addition of a persistent
176+
store integration. Before data has arrived from LaunchDarkly, the SDK is
177+
able to evaluate flags using data from the persistent store. Once fresh
178+
data is available, the SDK will no longer read from the persistent store,
179+
although it will keep it up-to-date.
180+
"""
181+
return default(config).data_store(store, DataStoreMode.READ_WRITE)
161182

183+
184+
# TODO(fdv2): Implement these methods
185+
#
162186
# WithEndpoints configures the data system with custom endpoints for
163187
# LaunchDarkly's streaming and polling synchronizers. This method is not
164188
# necessary for most use-cases, but can be useful for testing or custom

0 commit comments

Comments
 (0)