From e5500b34a04d0c77ba7c11aee2b2561c3d66aecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Behmo?= Date: Tue, 13 Oct 2020 19:57:13 +0200 Subject: [PATCH] Backport override features from edx-toggles to waffle_utils Note that those features are destined to be deprecated, eventually. --- .../core/djangoapps/waffle_utils/__init__.py | 202 ++++++------------ 1 file changed, 64 insertions(+), 138 deletions(-) diff --git a/openedx/core/djangoapps/waffle_utils/__init__.py b/openedx/core/djangoapps/waffle_utils/__init__.py index f9736b8e01..66750c49a0 100644 --- a/openedx/core/djangoapps/waffle_utils/__init__.py +++ b/openedx/core/djangoapps/waffle_utils/__init__.py @@ -1,186 +1,110 @@ """ -Utilities for waffle. +Extra utilities for waffle: most classes are defined in edx_toggles.toggles, but we keep here some extra classes for +usage within edx-platform. These classes cover course override use cases. -Includes namespacing, caching, and course overrides for waffle flags. Usage: -For Waffle Flags, first set up the namespace, and then create flags using the -namespace. For example:: - WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='my_namespace') - # Use CourseWaffleFlag when you are in the context of a course. SOME_COURSE_FLAG = CourseWaffleFlag(WAFFLE_FLAG_NAMESPACE, 'some_course_feature', __name__) - # Use WaffleFlag when outside the context of a course. - SOME_FLAG = WaffleFlag(WAFFLE_FLAG_NAMESPACE, 'some_feature', __name__) -You can check these flags in code using the following:: +You can check this flag in code using the following:: - SOME_FLAG.is_enabled() SOME_COURSE_FLAG.is_enabled(course_key) -To test these WaffleFlags, see testutils.py. - -In the above examples, you will use Django Admin "waffle" section to configure -for a flag named: my_namespace.some_course_feature - -You could also use the Django Admin "waffle_utils" section to configure a course -override for this same flag (e.g. my_namespace.some_course_feature). - -For Waffle Switches, first set up the namespace, and then create the flag name. -For example:: - - WAFFLE_SWITCHES = WaffleSwitchNamespace(name=WAFFLE_NAMESPACE) - - ESTIMATE_FIRST_ATTEMPTED = 'estimate_first_attempted' - -You can then use the switch as follows:: - - WAFFLE_SWITCHES.is_enabled(waffle.ESTIMATE_FIRST_ATTEMPTED) - To test WaffleSwitchNamespace, use the provided context managers. For example: with WAFFLE_SWITCHES.override(waffle.ESTIMATE_FIRST_ATTEMPTED, active=True): ... -For long-lived flags, you may want to change the default for devstack, sandboxes, -or new Open edX releases. For help with this, see: -openedx/core/djangoapps/waffle_utils/docs/decisions/0001-refactor-waffle-flag-default.rst - Also see ``WAFFLE_FLAG_CUSTOM_ATTRIBUTES`` and docstring for _set_waffle_flag_attribute for temporarily instrumenting/monitoring waffle flag usage. """ -from functools import lru_cache +import logging from contextlib import contextmanager -from weakref import WeakSet -from django.conf import settings -from edx_django_utils.monitoring import set_custom_attribute from opaque_keys.edx.keys import CourseKey from edx_toggles.toggles import WaffleFlag as BaseWaffleFlag -from edx_toggles.toggles import WaffleFlagNamespace as BaseWaffleFlagNamespace +from edx_toggles.toggles import WaffleFlagNamespace from edx_toggles.toggles import WaffleSwitch as BaseWaffleSwitch from edx_toggles.toggles import WaffleSwitchNamespace as BaseWaffleSwitchNamespace -from openedx.core.lib.cache_utils import get_cache as get_request_cache +log = logging.getLogger(__name__) class WaffleSwitchNamespace(BaseWaffleSwitchNamespace): """ - A waffle switch namespace class that implements request-based caching. + Waffle switch namespace that implements custom overriding methods. We should eventually get rid of this class. """ - @property - def _cached_switches(self): + @contextmanager + def override(self, switch_name, active=True): """ - Returns a dictionary of all namespaced switches in the request cache. + Overrides the active value for the given switch for the duration of this + contextmanager. + Note: The value is overridden in the request cache AND in the model. """ - return _get_waffle_namespace_request_cache().setdefault("switches", {}) + previous_active = self.is_enabled(switch_name) + try: + self.override_for_request(switch_name, active) + with self.override_in_model(switch_name, active): + yield + finally: + self.override_for_request(switch_name, previous_active) + def override_for_request(self, switch_name, active=True): + """ + Overrides the active value for the given switch for the remainder of + this request (as this is not a context manager). + Note: The value is overridden in the request cache, not in the model. + """ + namespaced_switch_name = self._namespaced_name(switch_name) + self._cached_switches[namespaced_switch_name] = active + log.info( + "%sSwitch '%s' set to %s for request.", + self.log_prefix, + namespaced_switch_name, + active, + ) -def _get_waffle_namespace_request_cache(): - """ - Returns a request cache shared by all Waffle namespace objects. - """ - return get_request_cache("WaffleNamespace") + @contextmanager + def override_in_model(self, switch_name, active=True): + """ + Overrides the active value for the given switch for the duration of this + contextmanager. + Note: The value is overridden in the model, not the request cache. + Note: This should probably be moved to a test class. + """ + # Import is placed here to avoid model import at project startup. + # pylint: disable=import-outside-toplevel + from waffle.testutils import override_switch as waffle_override_switch + + namespaced_switch_name = self._namespaced_name(switch_name) + with waffle_override_switch(namespaced_switch_name, active): + log.info( + "%sSwitch '%s' set to %s in model.", + self.log_prefix, + namespaced_switch_name, + active, + ) + yield class WaffleSwitch(BaseWaffleSwitch): """ - Waffle switch class that benefits from the additional features of the WaffleSwitchNamespace. + This class should be removed in favour of edx_toggles.toggles.WaffleSwitch once we get rid of the + WaffleSwitchNamespace class. """ NAMESPACE_CLASS = WaffleSwitchNamespace - -class WaffleFlagNamespace(BaseWaffleFlagNamespace): - """ - Namespace class that implements request-based caching. Also, setup some custom value-checking and processing. - """ - - @property - def _cached_flags(self): - """ - Returns a dictionary of all namespaced flags in the request cache. - """ - return _get_waffle_namespace_request_cache().setdefault("flags", {}) - - def _get_flag_active(self, namespaced_flag_name): - value = super()._get_flag_active(namespaced_flag_name) - _set_waffle_flag_attribute(namespaced_flag_name, value) - return value - - def _get_flag_active_default(self, namespaced_flag_name): - value = super()._get_flag_active_default(namespaced_flag_name) - set_custom_attribute("warn_flag_no_request_return_value", value) - return value - - -# .. toggle_name: WAFFLE_FLAG_CUSTOM_ATTRIBUTES -# .. toggle_implementation: DjangoSetting -# .. toggle_default: False -# .. toggle_description: A list of waffle flags to track with custom attributes having -# values of (True, False, or Both). -# .. toggle_use_cases: opt_in -# .. toggle_creation_date: 2020-06-17 -# .. toggle_warnings: Intent is for temporary research (1 day - several weeks) of a flag's usage. -@lru_cache(maxsize=None) -def _get_waffle_flag_custom_attributes_set(): - attributes = getattr(settings, "WAFFLE_FLAG_CUSTOM_ATTRIBUTES", None) or [] - return set(attributes) - - -def _set_waffle_flag_attribute(name, value): - """ - For any flag name in settings.WAFFLE_FLAG_CUSTOM_ATTRIBUTES, add name/value - to cached values and set custom attribute if the value changed. - - The name of the custom attribute will have the prefix ``flag_`` and the - suffix will match the name of the flag. - The value of the custom attribute could be False, True, or Both. - - The value Both would mean that the flag had both a True and False - value at different times during the transaction. This is most - likely due to having a course override, as is the case with - CourseWaffleFlag. - - An example NewRelic query to see the values of a flag in different - environments, if your waffle flag was named ``my.waffle.flag`` might - look like:: - - SELECT count(*) FROM Transaction - WHERE flag_my.waffle.flag IS NOT NULL - FACET appName, flag_my.waffle.flag - - Important: Remember to configure ``WAFFLE_FLAG_CUSTOM_ATTRIBUTES`` for - LMS, Studio and Workers in order to see waffle flag usage in all - edx-platform environments. - """ - if name not in _get_waffle_flag_custom_attributes_set(): - return - - flag_attribute_data = _get_waffle_namespace_request_cache().setdefault( - "flag_attribute", {} - ) - is_value_changed = True - if name not in flag_attribute_data: - # New flag - flag_attribute_data[name] = str(value) - else: - # Existing flag - if flag_attribute_data[name] == str(value): - # Same value - is_value_changed = False - else: - # New value - flag_attribute_data[name] = "Both" - - if is_value_changed: - attribute_name = "flag_{}".format(name) - set_custom_attribute(attribute_name, flag_attribute_data[name]) + @contextmanager + def override(self, active=True): + with self.waffle_namespace.override(self.switch_name, active): + yield class WaffleFlag(BaseWaffleFlag): @@ -190,7 +114,6 @@ class WaffleFlag(BaseWaffleFlag): This class should be removed in favour of edx_toggles.toggles.WaffleFlag once we get rid of the WaffleFlagNamespace class and the `override` method. """ - NAMESPACE_CLASS = WaffleFlagNamespace @contextmanager def override(self, active=True): @@ -271,6 +194,9 @@ class CourseWaffleFlag(WaffleFlag): ) is_enabled_for_course = self._get_course_override_value(course_key) if is_enabled_for_course is not None: - _set_waffle_flag_attribute(self.namespaced_flag_name, is_enabled_for_course) + # pylint: disable=protected-access + self.namespace._monitor_value( + self.namespaced_flag_name, is_enabled_for_course + ) return is_enabled_for_course return super().is_enabled()