|
2 | 2 | This submodule contains the client class that provides most of the SDK functionality. |
3 | 3 | """ |
4 | 4 |
|
5 | | -from typing import Optional, Any, Dict, Mapping, Union, Tuple |
| 5 | +from typing import Optional, Any, Dict, Mapping, Union, Tuple, Callable |
6 | 6 |
|
7 | 7 | from .impl import AnyNum |
8 | 8 |
|
|
21 | 21 | from ldclient.impl.datasource.polling import PollingUpdateProcessor |
22 | 22 | from ldclient.impl.datasource.streaming import StreamingUpdateProcessor |
23 | 23 | from ldclient.impl.datasource.status import DataSourceUpdateSinkImpl, DataSourceStatusProviderImpl |
| 24 | +from ldclient.impl.datastore.status import DataStoreUpdateSinkImpl, DataStoreStatusProviderImpl |
24 | 25 | from ldclient.impl.evaluator import Evaluator, error_reason |
25 | 26 | from ldclient.impl.events.diagnostics import create_diagnostic_id, _DiagnosticAccumulator |
26 | 27 | from ldclient.impl.events.event_processor import DefaultEventProcessor |
27 | 28 | from ldclient.impl.events.types import EventFactory |
28 | 29 | from ldclient.impl.model.feature_flag import FeatureFlag |
29 | 30 | from ldclient.impl.listeners import Listeners |
| 31 | +from ldclient.impl.rwlock import ReadWriteLock |
30 | 32 | from ldclient.impl.stubs import NullEventProcessor, NullUpdateProcessor |
31 | 33 | from ldclient.impl.util import check_uwsgi, log |
32 | | -from ldclient.interfaces import BigSegmentStoreStatusProvider, DataSourceStatusProvider, FeatureRequester, FeatureStore, FlagTracker |
| 34 | +from ldclient.impl.repeating_task import RepeatingTask |
| 35 | +from ldclient.interfaces import BigSegmentStoreStatusProvider, DataSourceStatusProvider, FeatureStore, FlagTracker, DataStoreUpdateSink, DataStoreStatus, DataStoreStatusProvider |
33 | 36 | from ldclient.versioned_data_kind import FEATURES, SEGMENTS, VersionedDataKind |
34 | | -from ldclient.feature_store import FeatureStore |
35 | 37 | from ldclient.migrations import Stage, OpTracker |
36 | 38 | from ldclient.impl.flag_tracker import FlagTrackerImpl |
37 | 39 |
|
38 | 40 | from threading import Lock |
39 | 41 |
|
40 | 42 |
|
41 | 43 |
|
| 44 | + |
42 | 45 | class _FeatureStoreClientWrapper(FeatureStore): |
43 | 46 | """Provides additional behavior that the client requires before or after feature store operations. |
44 | | - Currently this just means sorting the data set for init(). In the future we may also use this |
45 | | - to provide an update listener capability. |
| 47 | + Currently this just means sorting the data set for init() and dealing with data store status listeners. |
46 | 48 | """ |
47 | 49 |
|
48 | | - def __init__(self, store: FeatureStore): |
| 50 | + def __init__(self, store: FeatureStore, store_update_sink: DataStoreUpdateSink): |
49 | 51 | self.store = store |
| 52 | + self.__store_update_sink = store_update_sink |
| 53 | + self.__monitoring_enabled = self.is_monitoring_enabled() |
| 54 | + |
| 55 | + # Covers the following variables |
| 56 | + self.__lock = ReadWriteLock() |
| 57 | + self.__last_available = True |
| 58 | + self.__poller: Optional[RepeatingTask] = None |
50 | 59 |
|
51 | 60 | def init(self, all_data: Mapping[VersionedDataKind, Mapping[str, Dict[Any, Any]]]): |
52 | | - return self.store.init(_FeatureStoreDataSetSorter.sort_all_collections(all_data)) |
| 61 | + return self.__wrapper(lambda: self.store.init(_FeatureStoreDataSetSorter.sort_all_collections(all_data))) |
53 | 62 |
|
54 | 63 | def get(self, kind, key, callback): |
55 | | - return self.store.get(kind, key, callback) |
| 64 | + return self.__wrapper(lambda: self.store.get(kind, key, callback)) |
56 | 65 |
|
57 | 66 | def all(self, kind, callback): |
58 | | - return self.store.all(kind, callback) |
| 67 | + return self.__wrapper(lambda: self.store.all(kind, callback)) |
59 | 68 |
|
60 | 69 | def delete(self, kind, key, version): |
61 | | - return self.store.delete(kind, key, version) |
| 70 | + return self.__wrapper(lambda: self.store.delete(kind, key, version)) |
62 | 71 |
|
63 | 72 | def upsert(self, kind, item): |
64 | | - return self.store.upsert(kind, item) |
| 73 | + return self.__wrapper(lambda: self.store.upsert(kind, item)) |
65 | 74 |
|
66 | 75 | @property |
67 | 76 | def initialized(self) -> bool: |
68 | 77 | return self.store.initialized |
69 | 78 |
|
| 79 | + def __wrapper(self, fn: Callable): |
| 80 | + try: |
| 81 | + return fn() |
| 82 | + except BaseException: |
| 83 | + if self.__monitoring_enabled: |
| 84 | + self.__update_availability(False) |
| 85 | + raise |
| 86 | + |
| 87 | + def __update_availability(self, available: bool): |
| 88 | + try: |
| 89 | + self.__lock.lock() |
| 90 | + if available == self.__last_available: |
| 91 | + return |
| 92 | + self.__last_available = available |
| 93 | + finally: |
| 94 | + self.__lock.unlock() |
| 95 | + |
| 96 | + status = DataStoreStatus(available, False) |
| 97 | + |
| 98 | + if available: |
| 99 | + log.warn("Persistent store is available again") |
| 100 | + |
| 101 | + self.__store_update_sink.update_status(status) |
| 102 | + |
| 103 | + if available: |
| 104 | + try: |
| 105 | + self.__lock.lock() |
| 106 | + if self.__poller is not None: |
| 107 | + self.__poller.stop() |
| 108 | + self.__poller = None |
| 109 | + finally: |
| 110 | + self.__lock.unlock() |
| 111 | + |
| 112 | + return |
| 113 | + |
| 114 | + log.warn("Detected persistent store unavailability; updates will be cached until it recovers") |
| 115 | + task = RepeatingTask(0.5, 0, self.__check_availability) |
| 116 | + |
| 117 | + self.__lock.lock() |
| 118 | + self.__poller = task |
| 119 | + self.__poller.start() |
| 120 | + self.__lock.unlock() |
| 121 | + |
| 122 | + def __check_availability(self): |
| 123 | + try: |
| 124 | + if self.store.available: |
| 125 | + self.__update_availability(True) |
| 126 | + except BaseException as e: |
| 127 | + log.error("Unexpected error from data store status function: %s", e) |
| 128 | + |
| 129 | + def is_monitoring_enabled(self) -> bool: |
| 130 | + """ |
| 131 | + This methods determines whether the wrapped store can support enabling monitoring. |
| 132 | +
|
| 133 | + The wrapped store must provide a monitoring_enabled method, which must |
| 134 | + be true. But this alone is not sufficient. |
| 135 | +
|
| 136 | + Because this class wraps all interactions with a provided store, it can |
| 137 | + technically "monitor" any store. However, monitoring also requires that |
| 138 | + we notify listeners when the store is available again. |
| 139 | +
|
| 140 | + We determine this by checking the store's `available?` method, so this |
| 141 | + is also a requirement for monitoring support. |
| 142 | +
|
| 143 | + These extra checks won't be necessary once `available` becomes a part |
| 144 | + of the core interface requirements and this class no longer wraps every |
| 145 | + feature store. |
| 146 | + """ |
| 147 | + |
| 148 | + if not hasattr(self.store, 'is_monitoring_enabled'): |
| 149 | + return False |
| 150 | + |
| 151 | + if not hasattr(self.store, 'is_available'): |
| 152 | + return False |
| 153 | + |
| 154 | + monitoring_enabled = getattr(self.store, 'is_monitoring_enabled') |
| 155 | + if not callable(monitoring_enabled): |
| 156 | + return False |
| 157 | + |
| 158 | + return monitoring_enabled() |
| 159 | + |
70 | 160 |
|
71 | 161 | def _get_store_item(store, kind: VersionedDataKind, key: str) -> Any: |
72 | 162 | # This decorator around store.get provides backward compatibility with any custom data |
@@ -102,7 +192,11 @@ def __init__(self, config: Config, start_wait: float=5): |
102 | 192 | self._event_factory_default = EventFactory(False) |
103 | 193 | self._event_factory_with_reasons = EventFactory(True) |
104 | 194 |
|
105 | | - store = _FeatureStoreClientWrapper(self._config.feature_store) |
| 195 | + data_store_listeners = Listeners() |
| 196 | + store_sink = DataStoreUpdateSinkImpl(data_store_listeners) |
| 197 | + store = _FeatureStoreClientWrapper(self._config.feature_store, store_sink) |
| 198 | + |
| 199 | + self.__data_store_status_provider = DataStoreStatusProviderImpl(store, store_sink) |
106 | 200 |
|
107 | 201 | data_source_listeners = Listeners() |
108 | 202 | flag_change_listeners = Listeners() |
@@ -515,6 +609,21 @@ def data_source_status_provider(self) -> DataSourceStatusProvider: |
515 | 609 | """ |
516 | 610 | return self.__data_source_status_provider |
517 | 611 |
|
| 612 | + @property |
| 613 | + def data_store_status_provider(self) -> DataStoreStatusProvider: |
| 614 | + """ |
| 615 | + Returns an interface for tracking the status of a persistent data store. |
| 616 | +
|
| 617 | + The provider has methods for checking whether the data store is (as far |
| 618 | + as the SDK knows) currently operational, tracking changes in this |
| 619 | + status, and getting cache statistics. These are only relevant for a |
| 620 | + persistent data store; if you are using an in-memory data store, then |
| 621 | + this method will return a stub object that provides no information. |
| 622 | +
|
| 623 | + :return: The data store status provider |
| 624 | + """ |
| 625 | + return self.__data_store_status_provider |
| 626 | + |
518 | 627 | @property |
519 | 628 | def flag_tracker(self) -> FlagTracker: |
520 | 629 | """ |
|
0 commit comments