def generate_course_expired_fragment_from_key(user, course_key): """ Like `generate_course_expired_fragment`, but using a CourseKey instead of a CourseOverview and using request-level caching. Either returns WebFragment to inject XBlock content into, or None if we shouldn't show a course expired message for this user. """ request_cache = RequestCache('generate_course_expired_fragment_from_key') cache_key = u'message:{},{}'.format(user.id, course_key) cache_response = request_cache.get_cached_response(cache_key) if cache_response.is_found: cached_message = cache_response.value # In this case, there is no message to display. if cached_message is None: return None return generate_fragment_from_message(cached_message) course = CourseOverview.get_from_id(course_key) message = generate_course_expired_message(user, course) request_cache.set(cache_key, message) if message is None: return None return generate_fragment_from_message(message)
def _decorator(*args, **kwargs): """ Arguments: args, kwargs: values passed into the wrapped function """ # Check to see if we have a result in cache. If not, invoke our wrapped # function. Cache and return the result to the caller. if request_cache_getter: request_cache = request_cache_getter(args, kwargs) else: request_cache = RequestCache(namespace) if request_cache: cache_key = _func_call_cache_key(f, arg_map_function, *args, **kwargs) cached_response = request_cache.get_cached_response(cache_key) if cached_response.is_found: return cached_response.value result = f(*args, **kwargs) if request_cache: request_cache.set(cache_key, result) return result
def render(self, context=None, request=None): """ This takes a render call with a context (from Django) and translates it to a render call on the mako template. When rendering a large sequence of XBlocks, we may end up rendering hundreds of small templates. Even if context processors aren't very expensive individually, they will quickly add up in that situation. To help guard against this, we do context processing once for a given request and then cache it. """ context_object = self._get_context_object(request) request_cache = RequestCache('context_processors') cache_response = request_cache.get_cached_response('cp_output') if cache_response.is_found: context_dictionary = dict(cache_response.value) else: context_dictionary = self._get_context_processors_output_dict(context_object) # The context_dictionary is later updated with template specific # variables. There are potentially hundreds of calls to templates # rendering and we don't want them to interfere with each other, so # we make a copy from the output of the context processors and then # recreate a new dict every time we pull from the cache. request_cache.set('cp_output', dict(context_dictionary)) if isinstance(context, Context): context_dictionary.update(context.flatten()) elif context is not None: context_dictionary.update(context) self._add_core_context(context_dictionary) self._evaluate_lazy_csrf_tokens(context_dictionary) return self.mako_template.render_unicode(**context_dictionary)
def inner_wrapper(*args, **kwargs): """ Wrapper function to decorate with. """ # Check to see if we have a result in cache. If not, invoke our wrapped # function. Cache and return the result to the caller. request_cache = RequestCache(namespace) cache_key = _func_call_cache_key(f, *args, **kwargs) cached_response = request_cache.get_cached_response(cache_key) if cached_response.is_found: return cached_response.value result = f(*args, **kwargs) request_cache.set(cache_key, result) return result
def get_first_purchase_offer_banner_fragment_from_key(user, course_key): """ Like `get_first_purchase_offer_banner_fragment`, but using a CourseKey instead of a CourseOverview and using request-level caching. Either returns WebFragment to inject XBlock content into, or None if we shouldn't show a first purchase offer message for this user. """ request_cache = RequestCache('get_first_purchase_offer_banner_fragment_from_key') cache_key = u'html:{},{}'.format(user.id, course_key) cache_response = request_cache.get_cached_response(cache_key) if cache_response.is_found: cached_html = cache_response.value if cached_html is None: return None return Fragment(cached_html) course = CourseOverview.get_from_id(course_key) offer_html = generate_offer_html(user, course) request_cache.set(cache_key, offer_html) return Fragment(offer_html)
def _decorator(*args, **kwargs): """ Arguments: args, kwargs: values passed into the wrapped function """ # Check to see if we have a result in cache. If not, invoke our wrapped # function. Cache and return the result to the caller. if request_cache_getter: request_cache = request_cache_getter(args, kwargs) else: request_cache = RequestCache(namespace) if request_cache: cache_key = _func_call_cache_key(f, arg_map_function, *args, **kwargs) cached_response = request_cache.get_cached_response(cache_key) if cached_response.is_found: return cached_response.value result = f(*args, **kwargs) if request_cache: request_cache.set(cache_key, result) return result
def get_bucket(self, course_key=None, track=True): """ Return which bucket number the specified user is in. Bucket 0 is assumed to be the control bucket and will be returned if the experiment is not enabled for this user and course. """ # Keep some imports in here, because this class is commonly used at a module level, and we want to avoid # circular imports for any models. from experiments.models import ExperimentKeyValue from student.models import CourseEnrollment request = get_current_request() if not request: return 0 if not request.user.id: # We need username for stable bucketing and id for tracking, so just skip anonymous (not-logged-in) users return 0 # Use course key in experiment name to separate caches and segment calls per-course-run experiment_name = self.namespaced_flag_name + ('.{}'.format(course_key) if course_key else '') # Check if we have a cache for this request already request_cache = RequestCache('experiments') cache_response = request_cache.get_cached_response(experiment_name) if cache_response.is_found: return cache_response.value # Check if the main flag is even enabled for this user and course. if not self._is_enabled( course_key): # grabs user from the current request, if any return self._cache_bucket(experiment_name, 0) # Check if the enrollment should even be considered (if it started before the experiment wants, we ignore) if course_key and self.experiment_id is not None: start_val = ExperimentKeyValue.objects.filter( experiment_id=self.experiment_id, key='enrollment_start') if start_val: try: start_date = dateutil.parser.parse( start_val.first().value).replace(tzinfo=pytz.UTC) except ValueError: log.exception( 'Could not parse enrollment start date for experiment %d', self.experiment_id) return self._cache_bucket(experiment_name, 0) enrollment = CourseEnrollment.get_enrollment( request.user, course_key) # Only bail if they have an enrollment and it's old -- if they don't have an enrollment, we want to do # normal bucketing -- consider the case where the experiment has bits that show before you enroll. We # want to keep your bucketing stable before and after you do enroll. if enrollment and enrollment.created < start_date: return self._cache_bucket(experiment_name, 0) bucket = stable_bucketing_hash_group(experiment_name, self.num_buckets, request.user.username) # Now check if the user is forced into a particular bucket, using our subordinate bucket flags for i, bucket_flag in enumerate(self.bucket_flags): if bucket_flag.is_enabled(course_key): bucket = i break session_key = 'tracked.{}'.format(experiment_name) if track and hasattr(request, 'session') and session_key not in request.session: segment.track(user_id=request.user.id, event_name='edx.bi.experiment.user.bucketed', properties={ 'site': request.site.domain, 'app_label': self.waffle_namespace.name, 'experiment': self.flag_name, 'course_id': str(course_key) if course_key else None, 'bucket': bucket, 'is_staff': request.user.is_staff, 'nonInteraction': 1, }) # Mark that we've recorded this bucketing, so that we don't do it again this session request.session[session_key] = True return self._cache_bucket(experiment_name, bucket)
def _log_and_monitor_expected_errors(request, exception, caller): """ Adds logging and monitoring for expected errors as needed. Arguments: request: The request exception: The exception caller: Either 'middleware' or 'drf` """ expected_error_settings_dict = _get_expected_error_settings_dict() if not expected_error_settings_dict: return # 'module.Class', for example, 'django.core.exceptions.PermissionDenied' # Note: `Exception` itself doesn't have a module. exception_module = getattr(exception, '__module__', '') separator = '.' if exception_module else '' module_and_class = f'{exception_module}{separator}{exception.__class__.__name__}' # Set checked_error_expected_from custom attribute to potentially help find issues where errors are never processed. set_custom_attribute('checked_error_expected_from', caller) # check if we already added logging/monitoring from a different caller request_cache = RequestCache('openedx.core.lib.request_utils') cached_handled_exception = request_cache.get_cached_response( 'handled_exception') if cached_handled_exception.is_found: cached_module_and_class = cached_handled_exception.value # exception was already processed by a different caller if cached_handled_exception.value == module_and_class: set_custom_attribute('checked_error_expected_from', 'multiple') return # We have confirmed using monitoring that it is very rare that middleware and drf handle different uncaught exceptions. # We will leave this attribute in place, but it is not worth investing in a workaround, especially given that # New Relic now offers its own expected error functionality, and this functionality may be simplified or removed. set_custom_attribute('unexpected_multiple_exceptions', cached_module_and_class) log.warning( "Unexpected scenario where different exceptions are handled by _log_and_monitor_expected_errors. " "See 'unexpected_multiple_exceptions' custom attribute. Skipping exception for %s.", module_and_class, ) return request_cache.set('handled_exception', module_and_class) if module_and_class not in expected_error_settings_dict: return exception_message = str(exception) set_custom_attribute('error_expected', True) expected_error_settings = expected_error_settings_dict[module_and_class] if expected_error_settings['is_ignored']: # Additional error details are needed for ignored errors, because they are otherwise # not available by our monitoring system, because they have been ignored. set_custom_attribute('error_ignored_class', module_and_class) set_custom_attribute('error_ignored_message', exception_message) if expected_error_settings['log_error']: exc_info = exception if expected_error_settings[ 'log_stack_trace'] else None request_path = getattr(request, 'path', 'request-path-unknown') log.info( 'Expected error %s: %s: seen for path %s', module_and_class, exception_message, request_path, exc_info=exc_info, )
def get_bucket(self, course_key=None, track=True): """ Return which bucket number the specified user is in. The user may be force-bucketed if matching subordinate flags of the form "main_flag.BUCKET_NUM" exist. Otherwise, they will be hashed into a default bucket based on their username, the experiment name, and the course-run key. If `self.use_course_aware_bucketing` is False, the course-run key will be omitted from the hashing formula, thus making it so a given user has the same default bucket across all course runs; however, subordinate flags that match the course-run key will still apply. If `course_key` argument is omitted altogether, then subordinate flags will be evaluated outside of the course-run context, and the default bucket will be calculated as if `self.use_course_aware_bucketing` is False. Finally, Bucket 0 is assumed to be the control bucket and will be returned if the experiment is not enabled for this user and course. Arguments: course_key (Optional[CourseKey]) track (bool): Whether an analytics event should be generated if the user is bucketed for the first time. Returns: int """ # Keep some imports in here, because this class is commonly used at a module level, and we want to avoid # circular imports for any models. from lms.djangoapps.experiments.models import ExperimentKeyValue from lms.djangoapps.courseware.masquerade import get_specific_masquerading_user request = get_current_request() if not request: return 0 if not hasattr(request, 'user') or not request.user.id: # We need username for stable bucketing and id for tracking, so just skip anonymous (not-logged-in) users return 0 user = get_specific_masquerading_user(request.user, course_key) if user is None: user = request.user masquerading_as_specific_student = False else: masquerading_as_specific_student = True # If a course key is passed in, include it in the experiment name # in order to separate caches and analytics calls per course-run. # If we are using course-aware bucketing, then also append that course key # to `bucketing_group_name`, such that users can be hashed into different # buckets for different course-runs. experiment_name = bucketing_group_name = self.namespaced_flag_name if course_key: experiment_name += ".{}".format(course_key) if course_key and self.use_course_aware_bucketing: bucketing_group_name += ".{}".format(course_key) # Check if we have a cache for this request already request_cache = RequestCache('experiments') cache_response = request_cache.get_cached_response(experiment_name) if cache_response.is_found: return cache_response.value # Check if the main flag is even enabled for this user and course. if not self.is_experiment_on( course_key): # grabs user from the current request, if any return self._cache_bucket(experiment_name, 0) # Check if the enrollment should even be considered (if it started before the experiment wants, we ignore) if course_key and self.experiment_id is not None: values = ExperimentKeyValue.objects.filter( experiment_id=self.experiment_id).values('key', 'value') values = {pair['key']: pair['value'] for pair in values} if not self._is_enrollment_inside_date_bounds( values, user, course_key): return self._cache_bucket(experiment_name, 0) # Determine the user's bucket. # First check if forced into a particular bucket, using our subordinate bucket flags. # If not, calculate their default bucket using a consistent hash function. for i, bucket_flag in enumerate(self.bucket_flags): if bucket_flag.is_enabled(course_key): bucket = i break else: bucket = stable_bucketing_hash_group(bucketing_group_name, self.num_buckets, user.username) session_key = 'tracked.{}'.format(experiment_name) if (track and hasattr(request, 'session') and session_key not in request.session and not masquerading_as_specific_student): segment.track(user_id=user.id, event_name='edx.bi.experiment.user.bucketed', properties={ 'site': request.site.domain, 'app_label': self.waffle_namespace.name, 'experiment': self.flag_name, 'course_id': str(course_key) if course_key else None, 'bucket': bucket, 'is_staff': user.is_staff, 'nonInteraction': 1, }) # Mark that we've recorded this bucketing, so that we don't do it again this session request.session[session_key] = True return self._cache_bucket(experiment_name, bucket)
def get_bucket(self, course_key=None, track=True): """ Return which bucket number the specified user is in. Bucket 0 is assumed to be the control bucket and will be returned if the experiment is not enabled for this user and course. """ # Keep some imports in here, because this class is commonly used at a module level, and we want to avoid # circular imports for any models. from experiments.models import ExperimentKeyValue request = get_current_request() if not request: return 0 if not hasattr(request, 'user') or not request.user.id: # We need username for stable bucketing and id for tracking, so just skip anonymous (not-logged-in) users return 0 # Use course key in experiment name to separate caches and segment calls per-course-run experiment_name = self.namespaced_flag_name + ('.{}'.format(course_key) if course_key else '') # Check if we have a cache for this request already request_cache = RequestCache('experiments') cache_response = request_cache.get_cached_response(experiment_name) if cache_response.is_found: return cache_response.value # Check if the main flag is even enabled for this user and course. if not self.is_experiment_on( course_key): # grabs user from the current request, if any return self._cache_bucket(experiment_name, 0) # Check if the enrollment should even be considered (if it started before the experiment wants, we ignore) if course_key and self.experiment_id is not None: values = ExperimentKeyValue.objects.filter( experiment_id=self.experiment_id).values('key', 'value') values = {pair['key']: pair['value'] for pair in values} if not self._is_enrollment_inside_date_bounds( values, request.user, course_key): return self._cache_bucket(experiment_name, 0) bucket = stable_bucketing_hash_group(experiment_name, self.num_buckets, request.user.username) # Now check if the user is forced into a particular bucket, using our subordinate bucket flags for i, bucket_flag in enumerate(self.bucket_flags): if bucket_flag.is_enabled(course_key): bucket = i break session_key = 'tracked.{}'.format(experiment_name) if track and hasattr(request, 'session') and session_key not in request.session: segment.track(user_id=request.user.id, event_name='edx.bi.experiment.user.bucketed', properties={ 'site': request.site.domain, 'app_label': self.waffle_namespace.name, 'experiment': self.flag_name, 'course_id': str(course_key) if course_key else None, 'bucket': bucket, 'is_staff': request.user.is_staff, 'nonInteraction': 1, }) # Mark that we've recorded this bucketing, so that we don't do it again this session request.session[session_key] = True return self._cache_bucket(experiment_name, bucket)