Skip to content

Commit e772499

Browse files
authored
chore: Make the FDv2 feature-store client wrappers internal (#508)
1 parent 467da53 commit e772499

5 files changed

Lines changed: 170 additions & 175 deletions

File tree

ldclient/impl/datasystem/async_fdv2.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
from ldclient.versioned_data_kind import VersionedDataKind
5353

5454

55-
class AsyncFeatureStoreClientWrapper(AsyncFeatureStore):
55+
class _AsyncFeatureStoreClientWrapper(AsyncFeatureStore):
5656
"""Adds availability tracking around an async feature store.
5757
5858
Every store operation runs through a wrapper that watches for failures. When
@@ -261,7 +261,7 @@ def __init__(
261261
writable = data_system_config.data_store_mode == DataStoreMode.READ_WRITE
262262
# The async wrapper reports status through a plain callable sink, so
263263
# pass the provider's update method rather than the provider itself.
264-
wrapper = AsyncFeatureStoreClientWrapper(data_system_config.data_store, self._data_store_status_provider.update_status)
264+
wrapper = _AsyncFeatureStoreClientWrapper(data_system_config.data_store, self._data_store_status_provider.update_status)
265265
self._store.with_async_persistence(wrapper, writable, self._data_store_status_provider)
266266

267267
self._store_view = _AsyncReadOnlyStoreView(self._store)
@@ -646,7 +646,6 @@ async def data_availability(self) -> DataAvailability: # type: ignore[override]
646646

647647
__all__ = [
648648
'AsyncFDv2',
649-
'AsyncFeatureStoreClientWrapper',
650649
'ConditionDirective',
651650
'DataSourceStatusProviderImpl',
652651
'DataStoreStatusProviderImpl',

ldclient/impl/datasystem/fdv2.py

Lines changed: 152 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import time
22
from queue import Queue
33
from threading import Event, Thread
4-
from typing import Any, Callable, List, Optional
4+
from typing import Any, Callable, Dict, List, Mapping, Optional
55

66
from ldclient.config import Config, DataSourceBuilder, DataSystemConfig
7+
from ldclient.feature_store import _FeatureStoreDataSetSorter
78
from ldclient.impl.datasystem import DataSystem, DiagnosticSource
89
from ldclient.impl.datasystem.fdv2_common import (
910
ConditionDirective,
1011
DataSourceStatusProviderImpl,
1112
DataStoreStatusProviderImpl,
12-
FeatureStoreClientWrapper,
1313
_FDv2Base,
1414
fallback_condition,
1515
recovery_condition
@@ -25,6 +25,7 @@
2525
DataSourceState,
2626
DataStoreMode,
2727
DataStoreStatus,
28+
FeatureStore,
2829
ReadOnlyStore,
2930
Synchronizer
3031
)
@@ -56,6 +57,154 @@ def initialized(self) -> bool:
5657
return self._store.is_initialized()
5758

5859

60+
class _FeatureStoreClientWrapper(FeatureStore):
61+
"""Provides additional behavior that the client requires before or after feature store operations.
62+
Currently this just means sorting the data set for init() and dealing with data store status listeners.
63+
"""
64+
65+
def __init__(self, store: FeatureStore, store_update_sink: DataStoreStatusProviderImpl):
66+
self.store = store
67+
self.__store_update_sink = store_update_sink
68+
self.__monitoring_enabled = self.is_monitoring_enabled()
69+
70+
# Covers the following variables
71+
self.__lock = ReadWriteLock()
72+
self.__last_available = True
73+
self.__poller: Optional[RepeatingTask] = None
74+
self.__closed = False
75+
76+
def init(self, all_data: Mapping[VersionedDataKind, Mapping[str, Dict[Any, Any]]]):
77+
return self.__wrapper(lambda: self.store.init(_FeatureStoreDataSetSorter.sort_all_collections(all_data)))
78+
79+
def get(self, kind, key, callback):
80+
return self.__wrapper(lambda: self.store.get(kind, key, callback))
81+
82+
def all(self, kind, callback):
83+
return self.__wrapper(lambda: self.store.all(kind, callback))
84+
85+
def delete(self, kind, key, version):
86+
return self.__wrapper(lambda: self.store.delete(kind, key, version))
87+
88+
def upsert(self, kind, item):
89+
return self.__wrapper(lambda: self.store.upsert(kind, item))
90+
91+
@property
92+
def initialized(self) -> bool:
93+
return self.store.initialized
94+
95+
def disable_cache(self) -> None:
96+
def _do_disable():
97+
try:
98+
inner = self.store
99+
if hasattr(inner, "disable_cache"):
100+
inner.disable_cache() # type: ignore[attr-defined]
101+
except Exception as e:
102+
log.warning("disable_cache failed on inner store: %s", e)
103+
104+
self.__wrapper(_do_disable)
105+
106+
def __wrapper(self, fn: Callable):
107+
try:
108+
return fn()
109+
except BaseException:
110+
if self.__monitoring_enabled:
111+
self.__update_availability(False)
112+
raise
113+
114+
def __update_availability(self, available: bool):
115+
state_changed = False
116+
poller_to_stop = None
117+
task_to_start = None
118+
119+
with self.__lock.write():
120+
if self.__closed:
121+
return
122+
if available == self.__last_available:
123+
return
124+
125+
state_changed = True
126+
self.__last_available = available
127+
128+
if available:
129+
poller_to_stop = self.__poller
130+
self.__poller = None
131+
elif self.__poller is None:
132+
task_to_start = RepeatingTask("ldclient.check-availability", 0.5, 0, self.__check_availability)
133+
self.__poller = task_to_start
134+
135+
if available:
136+
log.warning("Persistent store is available again")
137+
else:
138+
log.warning("Detected persistent store unavailability; updates will be cached until it recovers")
139+
140+
status = DataStoreStatus(available, True)
141+
self.__store_update_sink.update_status(status)
142+
143+
if poller_to_stop is not None:
144+
poller_to_stop.stop()
145+
146+
if task_to_start is not None:
147+
task_to_start.start()
148+
149+
def __check_availability(self):
150+
try:
151+
if self.store.is_available():
152+
self.__update_availability(True)
153+
except BaseException as e:
154+
log.error("Unexpected error from data store status function: %s", e)
155+
156+
def is_monitoring_enabled(self) -> bool:
157+
"""
158+
This methods determines whether the wrapped store can support enabling monitoring.
159+
160+
The wrapped store must provide a monitoring_enabled method, which must
161+
be true. But this alone is not sufficient.
162+
163+
Because this class wraps all interactions with a provided store, it can
164+
technically "monitor" any store. However, monitoring also requires that
165+
we notify listeners when the store is available again.
166+
167+
We determine this by checking the store's `available?` method, so this
168+
is also a requirement for monitoring support.
169+
170+
These extra checks won't be necessary once `available` becomes a part
171+
of the core interface requirements and this class no longer wraps every
172+
feature store.
173+
"""
174+
175+
if not hasattr(self.store, 'is_monitoring_enabled'):
176+
return False
177+
178+
if not hasattr(self.store, 'is_available'):
179+
return False
180+
181+
monitoring_enabled = getattr(self.store, 'is_monitoring_enabled')
182+
if not callable(monitoring_enabled):
183+
return False
184+
185+
return monitoring_enabled()
186+
187+
def close(self):
188+
"""
189+
Close the wrapper and stop the repeating task poller if it's running.
190+
Also forwards the close call to the underlying store if it has a close method.
191+
"""
192+
poller_to_stop = None
193+
194+
with self.__lock.write():
195+
if self.__closed:
196+
return
197+
self.__closed = True
198+
poller_to_stop = self.__poller
199+
self.__poller = None
200+
201+
if poller_to_stop is not None:
202+
poller_to_stop.stop()
203+
204+
if hasattr(self.store, "close"):
205+
self.store.close()
206+
207+
59208
class FDv2(_FDv2Base, DataSystem):
60209
"""
61210
FDv2 is an implementation of the DataSystem interface that uses the Flag Delivery V2 protocol
@@ -92,7 +241,7 @@ def __init__(
92241
if data_system_config.data_store is not None:
93242
self._data_store_status_provider = DataStoreStatusProviderImpl(data_system_config.data_store, self._data_store_listeners)
94243
writable = data_system_config.data_store_mode == DataStoreMode.READ_WRITE
95-
wrapper = FeatureStoreClientWrapper(data_system_config.data_store, self._data_store_status_provider)
244+
wrapper = _FeatureStoreClientWrapper(data_system_config.data_store, self._data_store_status_provider)
96245
self._store.with_persistence(wrapper, writable, self._data_store_status_provider)
97246

98247
# Threading
@@ -489,5 +638,4 @@ def store(self) -> ReadOnlyStore:
489638
'DataSourceStatusProviderImpl',
490639
'DataStoreStatusProviderImpl',
491640
'FDv2',
492-
'FeatureStoreClientWrapper',
493641
]

0 commit comments

Comments
 (0)