Skip to content
Merged
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
5 changes: 5 additions & 0 deletions common/djangoapps/xblock_django/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@


from xblock_django.models import XBlockConfiguration, XBlockStudioConfiguration
from openedx.core.lib.cache_utils import CacheInvalidationManager

cacher = CacheInvalidationManager(model=XBlockConfiguration)


@cacher
def deprecated_xblocks():
"""
Return the QuerySet of deprecated XBlock types. Note that this method is independent of
Expand All @@ -14,6 +18,7 @@ def deprecated_xblocks():
return XBlockConfiguration.objects.current_set().filter(deprecated=True)


@cacher
def disabled_xblocks():
"""
Return the QuerySet of disabled XBlock types (which should not render in the LMS).
Expand Down
4 changes: 0 additions & 4 deletions common/djangoapps/xblock_django/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
from config_models.models import ConfigurationModel
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible


@python_2_unicode_compatible
class XBlockConfiguration(ConfigurationModel):
"""
XBlock configuration used by both LMS and Studio, and not specific to a particular template.
Expand All @@ -35,7 +33,6 @@ def __str__(self):
).format(self.name, self.enabled, self.deprecated)


@python_2_unicode_compatible
class XBlockStudioConfigurationFlag(ConfigurationModel):
"""
Enables site-wide Studio configuration for XBlocks.
Expand All @@ -52,7 +49,6 @@ def __str__(self):
return "XBlockStudioConfigurationFlag(enabled={})".format(self.enabled)


@python_2_unicode_compatible
class XBlockStudioConfiguration(ConfigurationModel):
"""
Studio editing configuration for a specific XBlock/template combination.
Expand Down
12 changes: 6 additions & 6 deletions lms/djangoapps/courseware/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS = 20

@ddt.data(
(ModuleStoreEnum.Type.mongo, 10, 176),
(ModuleStoreEnum.Type.split, 4, 174),
(ModuleStoreEnum.Type.mongo, 10, 172),
(ModuleStoreEnum.Type.split, 4, 170),
)
@ddt.unpack
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
Expand Down Expand Up @@ -1492,8 +1492,8 @@ def test_view_certificate_link_hidden(self):
self.assertContains(resp, u"Download Your Certificate")

@ddt.data(
(True, 54),
(False, 53)
(True, 53),
(False, 52)
)
@ddt.unpack
def test_progress_queries_paced_courses(self, self_paced, query_count):
Expand All @@ -1506,8 +1506,8 @@ def test_progress_queries_paced_courses(self, self_paced, query_count):

@patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})
@ddt.data(
(False, 62, 41),
(True, 53, 36)
(False, 61, 40),
(True, 52, 35)
)
@ddt.unpack
def test_progress_queries(self, enable_waffle, initial, subsequent):
Expand Down
2 changes: 2 additions & 0 deletions lms/djangoapps/courseware/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from django.views.decorators.http import require_GET, require_http_methods, require_POST
from django.views.generic import View
from edx_django_utils.monitoring import set_custom_metrics_for_course_key
from edxnotes.helpers import is_feature_enabled
from ipware.ip import get_ip
from markupsafe import escape
from opaque_keys import InvalidKeyError
Expand Down Expand Up @@ -1607,6 +1608,7 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True):
'disable_footer': True,
'disable_window_wrap': True,
'enable_completion_on_view_service': enable_completion_on_view_service,
'edx_notes_enabled': is_feature_enabled(course, request.user),
'staff_access': bool(request.user.has_perm(VIEW_XQA_INTERFACE, course)),
'xqa_server': settings.FEATURES.get('XQA_SERVER', 'http://your_xqa_server.com'),
}
Expand Down
6 changes: 3 additions & 3 deletions lms/djangoapps/grades/tests/test_course_grade_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,21 @@ def _assert_section_order(course_grade):
with self.assertNumQueries(3), mock_get_score(1, 2):
_assert_read(expected_pass=False, expected_percent=0) # start off with grade of 0

num_queries = 45
num_queries = 44
with self.assertNumQueries(num_queries), mock_get_score(1, 2):
grade_factory.update(self.request.user, self.course, force_update_subsections=True)

with self.assertNumQueries(3):
_assert_read(expected_pass=True, expected_percent=0.5) # updated to grade of .5

num_queries = 7
num_queries = 6
with self.assertNumQueries(num_queries), mock_get_score(1, 4):
grade_factory.update(self.request.user, self.course, force_update_subsections=False)

with self.assertNumQueries(3):
_assert_read(expected_pass=True, expected_percent=0.5) # NOT updated to grade of .25

num_queries = 24
num_queries = 23
with self.assertNumQueries(num_queries), mock_get_score(2, 2):
grade_factory.update(self.request.user, self.course, force_update_subsections=True)

Expand Down
4 changes: 2 additions & 2 deletions lms/djangoapps/instructor_task/tests/test_tasks_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ def test_query_counts(self, store_type, mongo_count):

RequestCache.clear_all_namespaces()

expected_query_count = 50
expected_query_count = 45
with patch('lms.djangoapps.instructor_task.tasks_helper.runner._get_current_task'):
with check_mongo_calls(mongo_count):
with self.assertNumQueries(expected_query_count):
Expand Down Expand Up @@ -2260,7 +2260,7 @@ def test_certificate_generation_for_students(self):
'failed': 3,
'skipped': 2
}
with self.assertNumQueries(146):
with self.assertNumQueries(141):
self.assertCertificatesGenerated(task_input, expected_results)

expected_results = {
Expand Down
8 changes: 4 additions & 4 deletions lms/templates/courseware/courseware-chromeless.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
<%namespace name='static' file='/static_content.html'/>
<%!
from django.utils.translation import ugettext as _
from edxnotes.helpers import is_feature_enabled as is_edxnotes_enabled

from openedx.core.djangolib.markup import HTML
from openedx.core.djangolib.js_utils import js_escaped_string

%>
<%def name="course_name()">
<% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %>
Expand Down Expand Up @@ -36,7 +36,7 @@
<%static:css group='style-course-vendor'/>
<%static:css group='style-course'/>
## Utility: Notes
% if is_edxnotes_enabled(course, request.user):
% if edx_notes_enabled:
<%static:css group='style-student-notes'/>
% endif

Expand Down Expand Up @@ -92,10 +92,10 @@
</main>
</section>
</div>
% if course.show_calculator or is_edxnotes_enabled(course, request.user):
% if course.show_calculator or edx_notes_enabled:
<nav class="nav-utilities ${"has-utility-calculator" if course.show_calculator else ""}" aria-label="${_('Course Utilities')}">
## Utility: Notes
% if is_edxnotes_enabled(course, request.user):
% if edx_notes_enabled:
<%include file="/edxnotes/toggle_notes.html" args="course=course"/>
% endif

Expand Down
2 changes: 1 addition & 1 deletion openedx/core/djangoapps/bookmarks/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def get_bookmarks(user, course_key=None, fields=None, serialized=True):
else:
bookmarks_queryset = bookmarks_queryset.select_related('user')

bookmarks_queryset = bookmarks_queryset.order_by('-created')
bookmarks_queryset = bookmarks_queryset.order_by('-id')
else:
bookmarks_queryset = Bookmark.objects.none()

Expand Down
16 changes: 16 additions & 0 deletions openedx/core/djangoapps/content/course_overviews/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from django.db import models, transaction
from django.db.models import Q
from django.db.models.fields import BooleanField, DateTimeField, DecimalField, FloatField, IntegerField, TextField
from django.db.models.signals import post_save, post_delete
from django.db.utils import IntegrityError
from django.template import defaultfilters
from django.utils.encoding import python_2_unicode_compatible
Expand All @@ -27,6 +28,7 @@
from openedx.core.djangoapps.catalog.models import CatalogIntegration
from openedx.core.djangoapps.lang_pref.api import get_closest_released_language
from openedx.core.djangoapps.models.course_details import CourseDetails
from openedx.core.lib.cache_utils import request_cached, RequestCache
from static_replace.models import AssetBaseUrlConfig
from xmodule import block_metadata_utils, course_metadata_utils
from xmodule.course_module import DEFAULT_START_DATE, CourseDescriptor
Expand Down Expand Up @@ -319,6 +321,7 @@ def course_exists(cls, course_id):
return modulestore().has_course(course_id)

@classmethod
@request_cached('course_overview')
def get_from_id(cls, course_id):
"""
Load a CourseOverview object for a given course ID.
Expand Down Expand Up @@ -1024,3 +1027,16 @@ class Meta(object):

def __str__(self):
return six.text_type(self.arguments)


def _invalidate_overview_cache(**kwargs): # pylint: disable=unused-argument
"""
Invalidate the course overview request cache.
"""
RequestCache('course_overview').clear()


post_save.connect(_invalidate_overview_cache, sender=CourseOverview)
post_save.connect(_invalidate_overview_cache, sender=CourseOverviewImageConfig)
post_delete.connect(_invalidate_overview_cache, sender=CourseOverview)
post_delete.connect(_invalidate_overview_cache, sender=CourseOverviewImageConfig)
2 changes: 2 additions & 0 deletions openedx/core/djangoapps/user_api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
# create an alias in "user_api".

from openedx.core.djangolib.model_mixins import DeletableByUserValue
from openedx.core.lib.cache_utils import request_cached
# pylint: disable=unused-import
from student.models import (
PendingEmailChange,
Expand Down Expand Up @@ -54,6 +55,7 @@ class Meta(object):
unique_together = ("user", "key")

@staticmethod
@request_cached()
def get_all_preferences(user):
"""
Gets all preferences for a given user
Expand Down
55 changes: 54 additions & 1 deletion openedx/core/lib/cache_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@

import six
import wrapt
from django.db.models.signals import post_save, post_delete
from django.utils.encoding import force_text
from edx_django_utils.cache import RequestCache

from edx_django_utils.cache import RequestCache, TieredCache
from six import iteritems
from six.moves import cPickle as pickle
from six.moves import map
Expand Down Expand Up @@ -151,6 +153,57 @@ def __get__(self, obj, objtype):
return functools.partial(self.__call__, obj)


class CacheInvalidationManager:
"""
This class provides a decorator for simple functions, which can handle invalidation.

To use, instantiate with a namespace or django model class:
`manager = CacheInvalidationManager(model=User)`

Then use it as a decorator on functions with no arguments
`@manager
def get_system_user():
...
`
When the User model is saved or deleted, all cache keys used by
the decorator will be cleared.
"""

def __init__(self, namespace=None, model=None, cache_time=86400):
if model:
post_save.connect(self.invalidate, sender=model)
post_delete.connect(self.invalidate, sender=model)
namespace = str(model)
self.namespace = namespace
self.cache_time = cache_time
self.keys = set()

# pylint: disable=unused-argument
def invalidate(self, **kwargs):
"""
Invalidate all keys tracked by the manager.
"""
for key in self.keys:
TieredCache.delete_all_tiers(key)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please change this to use the request cache instead of the tiered cached? Setting things in memcached across processes doesn't buy that much performance, but it could cause weird bugs because memcached lives outside the transaction lifetime of the database. So for instance, if we do a save on a model within a transaction, and that transaction later gets rolled back, using the tiered cache like this would have already done a set in memcached with that model value. Then that bad/rolled-back value would be alive in memcached for another process that runs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, that way we don't have to worry about what happens when we serialize into memcache an older version of a model that just had a column added to it, etc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this invalidation class in order to cache the XBlockConfiguration queryset, which rarely changes (and only through the deployment process or django admin, right?). I don't think there's much win by storing it in the request, but it seems appropriate to keep it in memcached.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. I was more worried about the long term if this started being used for things like enrollments. I agree this is safe to do for something like config models.

But if that's the use case, then I think we were hitting at the same problem from different sides. :-(

openedx/django-config-models#55

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that will be a good change. When will it land? Can I help?

But it won't solve this particular problem -- the xblock config is accessed like XBlockConfiguration.objects.current_set().filter(deprecated=True), which would bypass your caching. We could add similar caching to current_set(), or I could keep this for now, with a big TODO to remove it once there's caching in config models?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's close to merge, I just dropped it when we were getting close to the LabXchange launch and didn't pick it back up again. Let me do that today, as part of this story. I think I just need some minor fixes at this point.

I did think it addressed the XBlockConfiguration issue (at least most of them), because that was the major thing I was trying to eliminate when I was doing this optimization. But we should test it out in any event. Let me get that merged, then we can measure. I'll fix up that PR right now and tag you on review.


def __call__(self, func):
"""
Decorator for functions with no arguments.
"""
cache_key = '{}.{}.{}'.format(self.namespace, func.__module__, func.__name__)
self.keys.add(cache_key)

@functools.wraps(func)
def decorator(*args, **kwargs): # pylint: disable=unused-argument,missing-docstring
result = TieredCache.get_cached_response(cache_key)
if result.is_found:
return result.value
result = func()
TieredCache.set_all_tiers(cache_key, result, self.cache_time)
return result
return decorator


def zpickle(data):
"""Given any data structure, returns a zlib compressed pickled serialization."""
return zlib.compress(pickle.dumps(data, 4)) # Keep this constant as we upgrade from python 2 to 3.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def test_queries(self):

# Fetch the view and verify the query counts
# TODO: decrease query count as part of REVO-28
with self.assertNumQueries(74, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with self.assertNumQueries(73, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with check_mongo_calls(4):
url = course_home_url(self.course)
self.client.get(url)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def test_queries(self):

# Fetch the view and verify that the query counts haven't changed
# TODO: decrease query count as part of REVO-28
with self.assertNumQueries(51, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with self.assertNumQueries(50, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with check_mongo_calls(4):
url = course_updates_url(self.course)
self.client.get(url)