Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ldclient/async_feature_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,14 @@ async def init(self, all_data: Mapping[VersionedDataKind, Mapping[str, dict]]) -
for k in all_data:
log.debug("Initialized '%s' store with %d items", k.namespace, len(all_data[k]))

async def delete(self, kind: VersionedDataKind, key: str, version: int) -> None:
async def delete(self, kind: VersionedDataKind, key: str, version: int) -> bool:
""" """
items_of_kind = self._items[kind]
i = items_of_kind.get(key)
if i is None or i['version'] < version:
items_of_kind[key] = {'deleted': True, 'version': version}
return True
return False

async def upsert(self, kind: VersionedDataKind, item: dict) -> bool:
""" """
Expand Down
145 changes: 145 additions & 0 deletions ldclient/impl/datasource/async_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import time
from typing import Awaitable, Callable, Dict, Mapping, Optional, Set

from ldclient.impl.dependency_tracker import DependencyTracker, KindAndKey
from ldclient.impl.listeners import Listeners
from ldclient.impl.rwlock import ReadWriteLock
from ldclient.interfaces import (
AsyncDataSourceUpdateSink,
AsyncFeatureStore,
DataSourceErrorInfo,
DataSourceErrorKind,
DataSourceState,
DataSourceStatus,
FlagChange
)
from ldclient.versioned_data_kind import FEATURES, SEGMENTS, VersionedDataKind


class AsyncDataSourceUpdateSinkImpl(AsyncDataSourceUpdateSink):
def __init__(self, store: AsyncFeatureStore, status_listeners: Listeners, flag_change_listeners: Listeners):
self.__store = store
self.__status_listeners = status_listeners
self.__flag_change_listeners = flag_change_listeners
self.__tracker = DependencyTracker()

self.__lock = ReadWriteLock()
self.__status = DataSourceStatus(DataSourceState.INITIALIZING, time.time(), None)

@property
def status(self) -> DataSourceStatus:
with self.__lock.read():
return self.__status

async def init(self, all_data: Mapping[VersionedDataKind, Mapping[str, dict]]) -> None:
old_data: Optional[Dict[VersionedDataKind, Mapping[str, dict]]] = None

async def init_store():
nonlocal old_data
if self.__flag_change_listeners.has_listeners():
old_data = {}
for kind in [FEATURES, SEGMENTS]:
old_data[kind] = await self.__store.all(kind)
await self.__store.init(all_data)

await self.__monitor_store_update(init_store)

self.__reset_tracker_with_new_data(all_data)

if old_data is None:
return

self.__send_change_events(self.__compute_changed_items_for_full_data_set(old_data, all_data))

async def upsert(self, kind: VersionedDataKind, item: dict) -> None:
key = item.get('key', '')

updated = await self.__monitor_store_update(lambda: self.__store.upsert(kind, item))

# Only update dependency tracking and notify listeners if the store actually applied the
# update. The AsyncFeatureStore contract returns whether it wrote, so a stale
# (version-rejected) upsert produces no spurious flag-change events.
if updated:
self.__update_dependency_for_single_item(kind, key, item)

async def delete(self, kind: VersionedDataKind, key: str, version: int) -> None:
deleted = await self.__monitor_store_update(lambda: self.__store.delete(kind, key, version))

# Only notify when the store actually applied the delete, so a stale (version-rejected)
# delete produces no spurious flag-change events, mirroring the upsert path.
if deleted:
self.__update_dependency_for_single_item(kind, key, None)

def update_status(self, new_state: DataSourceState, new_error: Optional[DataSourceErrorInfo]) -> None:
status_to_broadcast = None

with self.__lock.write():
old_status = self.__status

if new_state == DataSourceState.INTERRUPTED and old_status.state == DataSourceState.INITIALIZING:
new_state = DataSourceState.INITIALIZING

if new_state == old_status.state and new_error is None:
return

self.__status = DataSourceStatus(
new_state,
self.__status.since if new_state == self.__status.state else time.time(),
self.__status.error if new_error is None else new_error,
)

status_to_broadcast = self.__status

if status_to_broadcast is not None:
self.__status_listeners.notify(status_to_broadcast)

async def __monitor_store_update(self, fn: Callable[[], Awaitable]):
try:
return await fn()
except Exception as e:
error_info = DataSourceErrorInfo(DataSourceErrorKind.STORE_ERROR, 0, time.time(), str(e))
self.update_status(DataSourceState.INTERRUPTED, error_info)
raise

def __update_dependency_for_single_item(self, kind: VersionedDataKind, key: str, item):
self.__tracker.update_dependencies_from(kind, key, item)
if self.__flag_change_listeners.has_listeners():
affected_items: Set[KindAndKey] = set()
self.__tracker.add_affected_items(affected_items, KindAndKey(kind=kind, key=key))
self.__send_change_events(affected_items)

def __reset_tracker_with_new_data(self, all_data: Mapping[VersionedDataKind, Mapping[str, dict]]):
self.__tracker.reset()
for kind, items in all_data.items():
for key, item in items.items():
self.__tracker.update_dependencies_from(kind, key, item)

def __send_change_events(self, affected_items: Set[KindAndKey]):
for item in affected_items:
if item.kind == FEATURES:
self.__flag_change_listeners.notify(FlagChange(item.key))

def __compute_changed_items_for_full_data_set(
self,
old_data: Mapping[VersionedDataKind, Mapping[str, dict]],
new_data: Mapping[VersionedDataKind, Mapping[str, dict]],
) -> Set[KindAndKey]:
affected_items: Set[KindAndKey] = set()

for kind in [FEATURES, SEGMENTS]:
old_items = old_data.get(kind, {})
new_items = new_data.get(kind, {})

keys: Set[str] = set()

for key in keys.union(old_items.keys(), new_items.keys()):
old_item = old_items.get(key)
new_item = new_items.get(key)

if old_item is None and new_item is None:
continue

if old_item is None or new_item is None or old_item['version'] < new_item['version']:
self.__tracker.add_affected_items(affected_items, KindAndKey(kind=kind, key=key))

return affected_items
Loading
Loading