Skip to content

Commit 149aa49

Browse files
committed
refactor: Move the sync FeatureStoreClientWrapper into the FDv2 data system
FeatureStoreClientWrapper lived in fdv2_common.py, which is meant for logic shared between the sync FDv2 and the async AsyncFDv2 (via _FDv2Base). This wrapper is sync-only — only fdv2.py uses it; the async side has its own wrapper in async_fdv2.py. Move it into fdv2.py, next to its sole user, and drop the now-unused imports and __all__ entry from fdv2_common.py. This mirrors the earlier FDv1 wrapper move (5948b78). Pure relocation, no behavior change.
1 parent 6a70132 commit 149aa49

2 files changed

Lines changed: 154 additions & 158 deletions

File tree

ldclient/impl/datasystem/fdv2.py

Lines changed: 151 additions & 2 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

ldclient/impl/datasystem/fdv2_common.py

Lines changed: 3 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,19 @@
22
Support classes shared by the sync and async FDv2 data system coordinators.
33
44
These are synchronous (thread-based) components used identically by both
5-
``FDv2`` and ``AsyncFDv2``: status providers, the persistent-store wrapper,
6-
and the condition directive enum.
5+
``FDv2`` and ``AsyncFDv2``: status providers, and the condition directive
6+
enum.
77
"""
88

99
import time
1010
from copy import copy
1111
from enum import Enum
12-
from typing import Any, Callable, Dict, Mapping, Optional
12+
from typing import Callable, Optional
1313

14-
from ldclient.feature_store import _FeatureStoreDataSetSorter
1514
from ldclient.impl.datasystem import DataAvailability, DiagnosticAccumulator
1615
from ldclient.impl.datasystem.store import _StoreBase
1716
from ldclient.impl.listeners import Listeners
18-
from ldclient.impl.repeating_task import RepeatingTask
1917
from ldclient.impl.rwlock import ReadWriteLock
20-
from ldclient.impl.util import log
2118
from ldclient.interfaces import (
2219
DataSourceErrorInfo,
2320
DataSourceState,
@@ -27,7 +24,6 @@
2724
DataStoreStatusProvider,
2825
FeatureStore
2926
)
30-
from ldclient.versioned_data_kind import VersionedDataKind
3127

3228

3329
class DataSourceStatusProviderImpl(DataSourceStatusProvider):
@@ -112,154 +108,6 @@ def remove_listener(self, listener: Callable[[DataStoreStatus], None]):
112108
self.__listeners.remove(listener)
113109

114110

115-
class FeatureStoreClientWrapper(FeatureStore):
116-
"""Provides additional behavior that the client requires before or after feature store operations.
117-
Currently this just means sorting the data set for init() and dealing with data store status listeners.
118-
"""
119-
120-
def __init__(self, store: FeatureStore, store_update_sink: DataStoreStatusProviderImpl):
121-
self.store = store
122-
self.__store_update_sink = store_update_sink
123-
self.__monitoring_enabled = self.is_monitoring_enabled()
124-
125-
# Covers the following variables
126-
self.__lock = ReadWriteLock()
127-
self.__last_available = True
128-
self.__poller: Optional[RepeatingTask] = None
129-
self.__closed = False
130-
131-
def init(self, all_data: Mapping[VersionedDataKind, Mapping[str, Dict[Any, Any]]]):
132-
return self.__wrapper(lambda: self.store.init(_FeatureStoreDataSetSorter.sort_all_collections(all_data)))
133-
134-
def get(self, kind, key, callback):
135-
return self.__wrapper(lambda: self.store.get(kind, key, callback))
136-
137-
def all(self, kind, callback):
138-
return self.__wrapper(lambda: self.store.all(kind, callback))
139-
140-
def delete(self, kind, key, version):
141-
return self.__wrapper(lambda: self.store.delete(kind, key, version))
142-
143-
def upsert(self, kind, item):
144-
return self.__wrapper(lambda: self.store.upsert(kind, item))
145-
146-
@property
147-
def initialized(self) -> bool:
148-
return self.store.initialized
149-
150-
def disable_cache(self) -> None:
151-
def _do_disable():
152-
try:
153-
inner = self.store
154-
if hasattr(inner, "disable_cache"):
155-
inner.disable_cache() # type: ignore[attr-defined]
156-
except Exception as e:
157-
log.warning("disable_cache failed on inner store: %s", e)
158-
159-
self.__wrapper(_do_disable)
160-
161-
def __wrapper(self, fn: Callable):
162-
try:
163-
return fn()
164-
except BaseException:
165-
if self.__monitoring_enabled:
166-
self.__update_availability(False)
167-
raise
168-
169-
def __update_availability(self, available: bool):
170-
state_changed = False
171-
poller_to_stop = None
172-
task_to_start = None
173-
174-
with self.__lock.write():
175-
if self.__closed:
176-
return
177-
if available == self.__last_available:
178-
return
179-
180-
state_changed = True
181-
self.__last_available = available
182-
183-
if available:
184-
poller_to_stop = self.__poller
185-
self.__poller = None
186-
elif self.__poller is None:
187-
task_to_start = RepeatingTask("ldclient.check-availability", 0.5, 0, self.__check_availability)
188-
self.__poller = task_to_start
189-
190-
if available:
191-
log.warning("Persistent store is available again")
192-
else:
193-
log.warning("Detected persistent store unavailability; updates will be cached until it recovers")
194-
195-
status = DataStoreStatus(available, True)
196-
self.__store_update_sink.update_status(status)
197-
198-
if poller_to_stop is not None:
199-
poller_to_stop.stop()
200-
201-
if task_to_start is not None:
202-
task_to_start.start()
203-
204-
def __check_availability(self):
205-
try:
206-
if self.store.is_available():
207-
self.__update_availability(True)
208-
except BaseException as e:
209-
log.error("Unexpected error from data store status function: %s", e)
210-
211-
def is_monitoring_enabled(self) -> bool:
212-
"""
213-
This methods determines whether the wrapped store can support enabling monitoring.
214-
215-
The wrapped store must provide a monitoring_enabled method, which must
216-
be true. But this alone is not sufficient.
217-
218-
Because this class wraps all interactions with a provided store, it can
219-
technically "monitor" any store. However, monitoring also requires that
220-
we notify listeners when the store is available again.
221-
222-
We determine this by checking the store's `available?` method, so this
223-
is also a requirement for monitoring support.
224-
225-
These extra checks won't be necessary once `available` becomes a part
226-
of the core interface requirements and this class no longer wraps every
227-
feature store.
228-
"""
229-
230-
if not hasattr(self.store, 'is_monitoring_enabled'):
231-
return False
232-
233-
if not hasattr(self.store, 'is_available'):
234-
return False
235-
236-
monitoring_enabled = getattr(self.store, 'is_monitoring_enabled')
237-
if not callable(monitoring_enabled):
238-
return False
239-
240-
return monitoring_enabled()
241-
242-
def close(self):
243-
"""
244-
Close the wrapper and stop the repeating task poller if it's running.
245-
Also forwards the close call to the underlying store if it has a close method.
246-
"""
247-
poller_to_stop = None
248-
249-
with self.__lock.write():
250-
if self.__closed:
251-
return
252-
self.__closed = True
253-
poller_to_stop = self.__poller
254-
self.__poller = None
255-
256-
if poller_to_stop is not None:
257-
poller_to_stop.stop()
258-
259-
if hasattr(self.store, "close"):
260-
self.store.close()
261-
262-
263111
class ConditionDirective(str, Enum):
264112
"""
265113
ConditionDirective represents the possible directives that can be returned from a condition check.
@@ -416,7 +264,6 @@ def target_availability(self) -> DataAvailability:
416264
'ConditionDirective',
417265
'DataSourceStatusProviderImpl',
418266
'DataStoreStatusProviderImpl',
419-
'FeatureStoreClientWrapper',
420267
'fallback_condition',
421268
'recovery_condition',
422269
]

0 commit comments

Comments
 (0)