-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Introduce flag change tracker api (#229)
The client instance will now provide access to a `flag_tracker`. This tracker allows developers to be notified when a flag configuration changes (or optionally when the /value/ of a flag changes for a particular context).
- Loading branch information
Showing
12 changed files
with
838 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
from ldclient.impl.model.feature_flag import FeatureFlag | ||
from ldclient.impl.model.segment import Segment | ||
from ldclient.impl.model.clause import Clause | ||
from ldclient.versioned_data_kind import VersionedDataKind, SEGMENTS, FEATURES | ||
|
||
from typing import Set, List, Dict, NamedTuple, Union, Optional | ||
|
||
|
||
class KindAndKey(NamedTuple): | ||
kind: VersionedDataKind | ||
key: str | ||
|
||
|
||
class DependencyTracker: | ||
""" | ||
The DependencyTracker is responsible for tracking both up and downstream | ||
dependency relationships. Managing a bi-directional mapping allows us to | ||
more easily perform updates to the tracker, and to determine affected items | ||
when a downstream item is modified. | ||
""" | ||
|
||
def __init__(self): | ||
self.__children: Dict[KindAndKey, Set[KindAndKey]] = {} | ||
self.__parents: Dict[KindAndKey, Set[KindAndKey]] = {} | ||
|
||
def update_dependencies_from(self, from_kind: VersionedDataKind, from_key: str, from_item: Optional[Union[dict, FeatureFlag, Segment]]): | ||
""" | ||
Updates the dependency graph when an item has changed. | ||
:param from_kind: the changed item's kind | ||
:param from_key: the changed item's key | ||
:param from_item: the changed item | ||
""" | ||
from_what = KindAndKey(kind=from_kind, key=from_key) | ||
updated_dependencies = DependencyTracker.compute_dependencies_from(from_kind, from_item) | ||
|
||
old_children_set = self.__children.get(from_what) | ||
|
||
if old_children_set is not None: | ||
for kind_and_key in old_children_set: | ||
parents_of_this_old_dep = self.__parents.get(kind_and_key, set()) | ||
if from_what in parents_of_this_old_dep: | ||
parents_of_this_old_dep.remove(from_what) | ||
|
||
self.__children[from_what] = updated_dependencies | ||
for kind_and_key in updated_dependencies: | ||
parents_of_this_new_dep = self.__parents.get(kind_and_key) | ||
if parents_of_this_new_dep is None: | ||
parents_of_this_new_dep = set() | ||
self.__parents[kind_and_key] = parents_of_this_new_dep | ||
|
||
parents_of_this_new_dep.add(from_what) | ||
|
||
def add_affected_items(self, items_out: Set[KindAndKey], initial_modified_item: KindAndKey): | ||
""" | ||
Populates the given set with the union of the initial item and all items that directly or indirectly | ||
depend on it (based on the current state of the dependency graph). | ||
@param items_out [Set] | ||
@param initial_modified_item [Object] | ||
""" | ||
|
||
if initial_modified_item in items_out: | ||
return | ||
|
||
items_out.add(initial_modified_item) | ||
|
||
parents = self.__parents.get(initial_modified_item) | ||
if parents is None: | ||
return | ||
|
||
for parent in parents: | ||
self.add_affected_items(items_out, parent) | ||
|
||
def reset(self): | ||
""" | ||
Clear any tracked dependencies and reset the tracking state to a clean slate. | ||
""" | ||
self.__children.clear() | ||
self.__parents.clear() | ||
|
||
@staticmethod | ||
def compute_dependencies_from(from_kind: VersionedDataKind, from_item: Optional[Union[dict, FeatureFlag, Segment]]) -> Set[KindAndKey]: | ||
""" | ||
@param from_kind [String] | ||
@param from_item [LaunchDarkly::Impl::Model::FeatureFlag, LaunchDarkly::Impl::Model::Segment] | ||
@return [Set] | ||
""" | ||
if from_item is None: | ||
return set() | ||
|
||
from_item = from_kind.decode(from_item) if isinstance(from_item, dict) else from_item | ||
|
||
if from_kind == FEATURES and isinstance(from_item, FeatureFlag): | ||
prereq_keys = [KindAndKey(kind=from_kind, key=p.key) for p in from_item.prerequisites] | ||
segment_keys = [kindAndKey for rule in from_item.rules for kindAndKey in DependencyTracker.segment_keys_from_clauses(rule.clauses)] | ||
|
||
results = set(prereq_keys) | ||
results.update(segment_keys) | ||
|
||
return results | ||
elif from_kind == SEGMENTS and isinstance(from_item, Segment): | ||
kind_and_keys = [key for rule in from_item.rules for key in DependencyTracker.segment_keys_from_clauses(rule.clauses)] | ||
return set(kind_and_keys) | ||
else: | ||
return set() | ||
|
||
@staticmethod | ||
def segment_keys_from_clauses(clauses: List[Clause]) -> List[KindAndKey]: | ||
results = [] | ||
for clause in clauses: | ||
if clause.op == 'segmentMatch': | ||
pairs = [KindAndKey(kind=SEGMENTS, key=value) for value in clause.values] | ||
results.extend(pairs) | ||
|
||
return results |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
from ldclient.interfaces import FlagTracker, FlagChange, FlagValueChange | ||
from ldclient.impl.listeners import Listeners | ||
from ldclient.context import Context | ||
from ldclient.impl.rwlock import ReadWriteLock | ||
|
||
from typing import Callable | ||
|
||
|
||
class FlagValueChangeListener: | ||
def __init__(self, key: str, context: Context, listener: Callable[[FlagValueChange], None], eval_fn: Callable): | ||
self.__key = key | ||
self.__context = context | ||
self.__listener = listener | ||
self.__eval_fn = eval_fn | ||
|
||
self.__lock = ReadWriteLock() | ||
self.__value = eval_fn(key, context) | ||
|
||
def __call__(self, flag_change: FlagChange): | ||
if flag_change.key != self.__key: | ||
return | ||
|
||
new_value = self.__eval_fn(self.__key, self.__context) | ||
|
||
self.__lock.lock() | ||
old_value, self.__value = self.__value, new_value | ||
self.__lock.unlock() | ||
|
||
if new_value == old_value: | ||
return | ||
|
||
self.__listener(FlagValueChange(self.__key, old_value, new_value)) | ||
|
||
|
||
class FlagTrackerImpl(FlagTracker): | ||
def __init__(self, listeners: Listeners, eval_fn: Callable): | ||
self.__listeners = listeners | ||
self.__eval_fn = eval_fn | ||
|
||
def add_listener(self, listener: Callable[[FlagChange], None]): | ||
self.__listeners.add(listener) | ||
|
||
def remove_listener(self, listener: Callable[[FlagChange], None]): | ||
self.__listeners.remove(listener) | ||
|
||
def add_flag_value_change_listener(self, key: str, context: Context, fn: Callable[[FlagValueChange], None]) -> Callable[[FlagChange], None]: | ||
listener = FlagValueChangeListener(key, context, fn, self.__eval_fn) | ||
self.add_listener(listener) | ||
|
||
return listener |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.