Skip to content

Commit

Permalink
Merge pull request #199 from launchdarkly/eb/sc-180417/model-classes
Browse files Browse the repository at this point in the history
(U2C 11) use custom classes for flag/segment data model
  • Loading branch information
eli-darkly authored Dec 20, 2022
2 parents 364e2eb + e4a478c commit 5cc102f
Show file tree
Hide file tree
Showing 30 changed files with 900 additions and 441 deletions.
14 changes: 11 additions & 3 deletions ldclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ def initialized(self) -> bool:
return self.store.initialized


def _get_store_item(store, kind: VersionedDataKind, key: str) -> Any:
# This decorator around store.get provides backward compatibility with any custom data
# store implementation that might still be returning a dict, instead of our data model
# classes like FeatureFlag.
item = store.get(kind, key, lambda x: x)
return kind.decode(item) if isinstance(item, dict) else item


class LDClient:
"""The LaunchDarkly SDK client object.
Expand Down Expand Up @@ -96,8 +104,8 @@ def __init__(self, config: Config, start_wait: float=5):
self.__big_segment_store_manager = big_segment_store_manager

self._evaluator = Evaluator(
lambda key: store.get(FEATURES, key, lambda x: x),
lambda key: store.get(SEGMENTS, key, lambda x: x),
lambda key: _get_store_item(store, FEATURES, key),
lambda key: _get_store_item(store, SEGMENTS, key),
lambda key: big_segment_store_manager.get_user_membership(key)
)

Expand Down Expand Up @@ -309,7 +317,7 @@ def _evaluate_internal(self, key: str, context: Union[Context, dict], default: A
return EvaluationDetail(default, None, error_reason('USER_NOT_SPECIFIED'))

try:
flag = self._store.get(FEATURES, key, lambda x: x)
flag = _get_store_item(self._store, FEATURES, key)
except Exception as e:
log.error("Unexpected error while retrieving feature flag \"%s\": %s" % (key, repr(e)))
log.debug(traceback.format_exc())
Expand Down
11 changes: 9 additions & 2 deletions ldclient/feature_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,16 @@ def all(self, kind, callback):
def init(self, all_data):
"""
"""
all_decoded = {}
for kind, items in all_data.items():
items_decoded = {}
for key, item in items.items():
items_decoded[key] = kind.decode(item)
all_decoded[kind] = items_decoded
try:
self._lock.rlock()
self._items.clear()
self._items.update(all_data)
self._items.update(all_decoded)
self._initialized = True
for k in all_data:
log.debug("Initialized '%s' store with %d items", k.namespace, len(all_data[k]))
Expand All @@ -135,13 +141,14 @@ def delete(self, kind, key: str, version: int):
def upsert(self, kind, item):
"""
"""
decoded_item = kind.decode(item)
key = item['key']
try:
self._lock.rlock()
itemsOfKind = self._items[kind]
i = itemsOfKind.get(key)
if i is None or i['version'] < item['version']:
itemsOfKind[key] = item
itemsOfKind[key] = decoded_item
log.debug("Updated %s in '%s' to version %d", key, kind.namespace, item['version'])
finally:
self._lock.runlock()
Expand Down
49 changes: 31 additions & 18 deletions ldclient/feature_store_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
from ldclient.versioned_data_kind import VersionedDataKind
from ldclient.feature_store import CacheConfig

def _ensure_encoded(kind, item):
return item if isinstance(item, dict) else kind.encode(item)

def _is_deleted(item):
return item is not None and item.get('deleted') is True

class CachingStoreWrapper(DiagnosticDescription, FeatureStore):
"""A partial implementation of :class:`ldclient.interfaces.FeatureStore`.
Expand All @@ -32,16 +38,20 @@ def __init__(self, core: FeatureStoreCore, cache_config: CacheConfig):
self._cache = None
self._inited = False

def init(self, all_data: Mapping[VersionedDataKind, Mapping[str, Dict[Any, Any]]]):
def init(self, all_encoded_data: Mapping[VersionedDataKind, Mapping[str, Dict[Any, Any]]]):
"""
"""
self._core.init_internal(all_data)
self._core.init_internal(all_encoded_data) # currently FeatureStoreCore expects to receive dicts
if self._cache is not None:
self._cache.clear()
for kind, items in all_data.items():
self._cache[self._all_cache_key(kind)] = self._items_if_not_deleted(items)
for kind, items in all_encoded_data.items():
decoded_items = {} # we don't want to cache dicts, we want to cache FeatureFlags/Segments
for key, item in items.items():
self._cache[self._item_cache_key(kind, key)] = [item] # note array wrapper
decoded_item = kind.decode(item)
self._cache[self._item_cache_key(kind, key)] = [decoded_item] # note array wrapper
if not _is_deleted(decoded_item):
decoded_items[key] = decoded_item
self._cache[self._all_cache_key(kind)] = decoded_items
self._inited = True

def get(self, kind, key, callback=lambda x: x):
Expand All @@ -52,11 +62,13 @@ def get(self, kind, key, callback=lambda x: x):
cached_item = self._cache.get(cache_key)
# note, cached items are wrapped in an array so we can cache None values
if cached_item is not None:
return callback(self._item_if_not_deleted(cached_item[0]))
item = self._core.get_internal(kind, key)
item = cached_item[0]
return callback(None if _is_deleted(item) else item)
encoded_item = self._core.get_internal(kind, key) # currently FeatureStoreCore returns dicts
item = None if encoded_item is None else kind.decode(encoded_item)
if self._cache is not None:
self._cache[cache_key] = [item]
return callback(self._item_if_not_deleted(item))
return callback(None if _is_deleted(item) else item)

def all(self, kind, callback=lambda x: x):
"""
Expand All @@ -66,7 +78,12 @@ def all(self, kind, callback=lambda x: x):
cached_items = self._cache.get(cache_key)
if cached_items is not None:
return callback(cached_items)
items = self._items_if_not_deleted(self._core.get_all_internal(kind))
encoded_items = self._core.get_all_internal(kind)
all_items = {}
if encoded_items is not None:
for key, item in encoded_items.items():
all_items[key] = kind.decode(item)
items = self._items_if_not_deleted(all_items)
if self._cache is not None:
self._cache[cache_key] = items
return callback(items)
Expand All @@ -77,12 +94,14 @@ def delete(self, kind, key, version):
deleted_item = { "key": key, "version": version, "deleted": True }
self.upsert(kind, deleted_item)

def upsert(self, kind, item):
def upsert(self, kind, encoded_item):
"""
"""
new_state = self._core.upsert_internal(kind, item)
encoded_item = _ensure_encoded(kind, encoded_item)
new_state = self._core.upsert_internal(kind, encoded_item)
new_decoded_item = kind.decode(new_state)
if self._cache is not None:
self._cache[self._item_cache_key(kind, item.get('key'))] = [new_state]
self._cache[self._item_cache_key(kind, new_decoded_item.get('key'))] = [new_decoded_item]
self._cache.pop(self._all_cache_key(kind), None)

@property
Expand Down Expand Up @@ -115,12 +134,6 @@ def _item_cache_key(kind, key):
def _all_cache_key(kind):
return kind.namespace

@staticmethod
def _item_if_not_deleted(item):
if item is not None and item.get('deleted', False):
return None
return item

@staticmethod
def _items_if_not_deleted(items):
results = {}
Expand Down
Loading

0 comments on commit 5cc102f

Please sign in to comment.