Commit b1b950c6 by Nimisha Asthagiri Committed by GitHub

Merge pull request #14770 from edx/neem/waffle-utils

Waffle Switch with namespacing and request caching
parents e9c00174 4ac78706
...@@ -196,6 +196,8 @@ simplefilter('ignore') ...@@ -196,6 +196,8 @@ simplefilter('ignore')
CELERY_ALWAYS_EAGER = True CELERY_ALWAYS_EAGER = True
CELERY_RESULT_BACKEND = 'djcelery.backends.cache:CacheBackend' CELERY_RESULT_BACKEND = 'djcelery.backends.cache:CacheBackend'
CLEAR_REQUEST_CACHE_ON_TASK_COMPLETION = False
########################### Server Ports ################################### ########################### Server Ports ###################################
# These ports are carefully chosen so that if the browser needs to # These ports are carefully chosen so that if the browser needs to
......
...@@ -25,7 +25,8 @@ def clear_request_cache(**kwargs): # pylint: disable=unused-argument ...@@ -25,7 +25,8 @@ def clear_request_cache(**kwargs): # pylint: disable=unused-argument
Once a celery task completes, clear the request cache to Once a celery task completes, clear the request cache to
prevent memory leaks. prevent memory leaks.
""" """
middleware.RequestCache.clear_request_cache() if getattr(settings, 'CLEAR_REQUEST_CACHE_ON_TASK_COMPLETION', True):
middleware.RequestCache.clear_request_cache()
def get_cache(name): def get_cache(name):
......
...@@ -4,6 +4,7 @@ Tests for the request cache. ...@@ -4,6 +4,7 @@ Tests for the request cache.
from celery.task import task from celery.task import task
from django.conf import settings from django.conf import settings
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings
from mock import Mock from mock import Mock
from request_cache import get_request_or_stub from request_cache import get_request_or_stub
...@@ -31,6 +32,7 @@ class TestRequestCache(TestCase): ...@@ -31,6 +32,7 @@ class TestRequestCache(TestCase):
cache = {"course_cache": "blah blah blah"} cache = {"course_cache": "blah blah blah"}
modulestore().request_cache.data.update(cache) modulestore().request_cache.data.update(cache)
@override_settings(CLEAR_REQUEST_CACHE_ON_TASK_COMPLETION=True)
def test_clear_cache_celery(self): def test_clear_cache_celery(self):
""" Test that the request cache is cleared after a task is run. """ """ Test that the request cache is cleared after a task is run. """
self._dummy_task.apply(args=(self,)).get() self._dummy_task.apply(args=(self,)).get()
......
...@@ -7,8 +7,7 @@ from django.test.client import RequestFactory ...@@ -7,8 +7,7 @@ from django.test.client import RequestFactory
from itertools import product from itertools import product
from openedx.core.djangoapps.content.block_structure.api import clear_course_from_cache from openedx.core.djangoapps.content.block_structure.api import clear_course_from_cache
from openedx.core.djangoapps.content.block_structure.config import STORAGE_BACKING_FOR_CACHE from openedx.core.djangoapps.content.block_structure.config import STORAGE_BACKING_FOR_CACHE, waffle
from openedx.core.djangoapps.content.block_structure.tests.helpers import override_config_setting
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
...@@ -142,12 +141,12 @@ class TestGetBlocksQueryCounts(SharedModuleStoreTestCase): ...@@ -142,12 +141,12 @@ class TestGetBlocksQueryCounts(SharedModuleStoreTestCase):
) )
@ddt.unpack @ddt.unpack
def test_query_counts_cached(self, store_type, with_storage_backing): def test_query_counts_cached(self, store_type, with_storage_backing):
with override_config_setting(STORAGE_BACKING_FOR_CACHE, active=with_storage_backing): with waffle().override(STORAGE_BACKING_FOR_CACHE, active=with_storage_backing):
course = self._create_course(store_type) course = self._create_course(store_type)
self._get_blocks( self._get_blocks(
course, course,
expected_mongo_queries=0, expected_mongo_queries=0,
expected_sql_queries=4 if with_storage_backing else 3, expected_sql_queries=3 if with_storage_backing else 2,
) )
@ddt.data( @ddt.data(
...@@ -159,7 +158,7 @@ class TestGetBlocksQueryCounts(SharedModuleStoreTestCase): ...@@ -159,7 +158,7 @@ class TestGetBlocksQueryCounts(SharedModuleStoreTestCase):
@ddt.unpack @ddt.unpack
def test_query_counts_uncached(self, store_type_tuple, with_storage_backing): def test_query_counts_uncached(self, store_type_tuple, with_storage_backing):
store_type, expected_mongo_queries = store_type_tuple store_type, expected_mongo_queries = store_type_tuple
with override_config_setting(STORAGE_BACKING_FOR_CACHE, active=with_storage_backing): with waffle().override(STORAGE_BACKING_FOR_CACHE, active=with_storage_backing):
course = self._create_course(store_type) course = self._create_course(store_type)
clear_course_from_cache(course.id) clear_course_from_cache(course.id)
self._get_blocks( self._get_blocks(
......
...@@ -333,6 +333,8 @@ GIT_REPO_DIR = TEST_ROOT / "course_repos" ...@@ -333,6 +333,8 @@ GIT_REPO_DIR = TEST_ROOT / "course_repos"
CELERY_ALWAYS_EAGER = True CELERY_ALWAYS_EAGER = True
CELERY_RESULT_BACKEND = 'djcelery.backends.cache:CacheBackend' CELERY_RESULT_BACKEND = 'djcelery.backends.cache:CacheBackend'
CLEAR_REQUEST_CACHE_ON_TASK_COMPLETION = False
######################### MARKETING SITE ############################### ######################### MARKETING SITE ###############################
MKTG_URL_LINK_MAP = { MKTG_URL_LINK_MAP = {
......
...@@ -2,43 +2,27 @@ ...@@ -2,43 +2,27 @@
This module contains various configuration settings via This module contains various configuration settings via
waffle switches for the Block Structure framework. waffle switches for the Block Structure framework.
""" """
import logging from openedx.core.djangolib.waffle_utils import WaffleSwitchPlus
from request_cache.middleware import request_cached
from openedx.core.djangolib.waffle_utils import is_switch_enabled
from request_cache.middleware import request_cached, RequestCache, func_call_cache_key
from .models import BlockStructureConfiguration from .models import BlockStructureConfiguration
log = logging.getLogger(__name__) # Namespace
WAFFLE_NAMESPACE = u'block_structure'
# Switches
INVALIDATE_CACHE_ON_PUBLISH = u'invalidate_cache_on_publish' INVALIDATE_CACHE_ON_PUBLISH = u'invalidate_cache_on_publish'
STORAGE_BACKING_FOR_CACHE = u'storage_backing_for_cache' STORAGE_BACKING_FOR_CACHE = u'storage_backing_for_cache'
RAISE_ERROR_WHEN_NOT_FOUND = u'raise_error_when_not_found' RAISE_ERROR_WHEN_NOT_FOUND = u'raise_error_when_not_found'
PRUNE_OLD_VERSIONS = u'prune_old_versions' PRUNE_OLD_VERSIONS = u'prune_old_versions'
def is_enabled(setting_name): def waffle():
"""
Returns whether the given block_structure setting
is enabled.
"""
bs_waffle_name = _bs_waffle_switch_name(setting_name)
return is_switch_enabled(bs_waffle_name)
def enable_for_current_request(setting_name):
""" """
Enables the given block_structure setting for the Returns the namespaced and cached Waffle class for BlockStructures.
duration of the current request.
""" """
cache_key = func_call_cache_key( return WaffleSwitchPlus(namespace=WAFFLE_NAMESPACE, log_prefix=u'BlockStructure: ')
is_switch_enabled.request_cached_contained_func,
_bs_waffle_switch_name(setting_name),
)
RequestCache.get_request_cache().data[cache_key] = True
log.warning(u'BlockStructure: Config %s is enabled for current request.', setting_name)
@request_cached @request_cached
...@@ -55,11 +39,3 @@ def cache_timeout_in_seconds(): ...@@ -55,11 +39,3 @@ def cache_timeout_in_seconds():
Returns and caches the current setting for cache_timeout_in_seconds. Returns and caches the current setting for cache_timeout_in_seconds.
""" """
return BlockStructureConfiguration.current().cache_timeout_in_seconds return BlockStructureConfiguration.current().cache_timeout_in_seconds
def _bs_waffle_switch_name(setting_name):
"""
Returns the name of the waffle switch for the
given block structure setting.
"""
return u'block_structure.{}'.format(setting_name)
...@@ -7,7 +7,7 @@ from django.core.management.base import BaseCommand ...@@ -7,7 +7,7 @@ from django.core.management.base import BaseCommand
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
import openedx.core.djangoapps.content.block_structure.api as api import openedx.core.djangoapps.content.block_structure.api as api
from openedx.core.djangoapps.content.block_structure.config import STORAGE_BACKING_FOR_CACHE, enable_for_current_request from openedx.core.djangoapps.content.block_structure.config import STORAGE_BACKING_FOR_CACHE, waffle
import openedx.core.djangoapps.content.block_structure.tasks as tasks import openedx.core.djangoapps.content.block_structure.tasks as tasks
import openedx.core.djangoapps.content.block_structure.store as store import openedx.core.djangoapps.content.block_structure.store as store
from openedx.core.lib.command_utils import ( from openedx.core.lib.command_utils import (
...@@ -127,7 +127,7 @@ class Command(BaseCommand): ...@@ -127,7 +127,7 @@ class Command(BaseCommand):
Generates course blocks for the given course_keys per the given options. Generates course blocks for the given course_keys per the given options.
""" """
if options.get('with_storage'): if options.get('with_storage'):
enable_for_current_request(STORAGE_BACKING_FOR_CACHE) waffle().override_for_request(STORAGE_BACKING_FOR_CACHE)
for course_key in course_keys: for course_key in course_keys:
try: try:
......
...@@ -98,7 +98,7 @@ class BlockStructureManager(object): ...@@ -98,7 +98,7 @@ class BlockStructureManager(object):
BlockStructureTransformers.verify_versions(block_structure) BlockStructureTransformers.verify_versions(block_structure)
except (BlockStructureNotFound, TransformerDataIncompatible): except (BlockStructureNotFound, TransformerDataIncompatible):
if config.is_enabled(config.RAISE_ERROR_WHEN_NOT_FOUND): if config.waffle().is_enabled(config.RAISE_ERROR_WHEN_NOT_FOUND):
raise raise
else: else:
block_structure = self._update_collected() block_structure = self._update_collected()
......
...@@ -226,7 +226,7 @@ class BlockStructureModel(TimeStampedModel): ...@@ -226,7 +226,7 @@ class BlockStructureModel(TimeStampedModel):
""" """
Deletes previous file versions for data_usage_key. Deletes previous file versions for data_usage_key.
""" """
if not config.is_enabled(config.PRUNE_OLD_VERSIONS): if not config.waffle().is_enabled(config.PRUNE_OLD_VERSIONS):
return return
if num_to_keep is None: if num_to_keep is None:
......
...@@ -23,7 +23,7 @@ def _update_block_structure_on_course_publish(sender, course_key, **kwargs): # ...@@ -23,7 +23,7 @@ def _update_block_structure_on_course_publish(sender, course_key, **kwargs): #
if isinstance(course_key, LibraryLocator): if isinstance(course_key, LibraryLocator):
return return
if config.is_enabled(config.INVALIDATE_CACHE_ON_PUBLISH): if config.waffle().is_enabled(config.INVALIDATE_CACHE_ON_PUBLISH):
clear_course_from_cache(course_key) clear_course_from_cache(course_key)
update_course_in_cache_v2.apply_async( update_course_in_cache_v2.apply_async(
......
...@@ -254,4 +254,4 @@ def _is_storage_backing_enabled(): ...@@ -254,4 +254,4 @@ def _is_storage_backing_enabled():
""" """
Returns whether storage backing for Block Structures is enabled. Returns whether storage backing for Block Structures is enabled.
""" """
return config.is_enabled(config.STORAGE_BACKING_FOR_CACHE) return config.waffle().is_enabled(config.STORAGE_BACKING_FOR_CACHE)
...@@ -13,7 +13,7 @@ from opaque_keys.edx.keys import CourseKey ...@@ -13,7 +13,7 @@ from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from openedx.core.djangoapps.content.block_structure import api from openedx.core.djangoapps.content.block_structure import api
from openedx.core.djangoapps.content.block_structure.config import STORAGE_BACKING_FOR_CACHE, enable_for_current_request from openedx.core.djangoapps.content.block_structure.config import STORAGE_BACKING_FOR_CACHE, waffle
log = logging.getLogger('edx.celery.task') log = logging.getLogger('edx.celery.task')
...@@ -59,7 +59,7 @@ def _update_course_in_cache(self, **kwargs): ...@@ -59,7 +59,7 @@ def _update_course_in_cache(self, **kwargs):
Updates the course blocks (mongo -> BlockStructure) for the specified course. Updates the course blocks (mongo -> BlockStructure) for the specified course.
""" """
if kwargs.get('with_storage'): if kwargs.get('with_storage'):
enable_for_current_request(STORAGE_BACKING_FOR_CACHE) waffle().override_for_request(STORAGE_BACKING_FOR_CACHE)
_call_and_retry_if_needed(self, api.update_course_in_cache, **kwargs) _call_and_retry_if_needed(self, api.update_course_in_cache, **kwargs)
...@@ -88,7 +88,7 @@ def _get_course_in_cache(self, **kwargs): ...@@ -88,7 +88,7 @@ def _get_course_in_cache(self, **kwargs):
Gets the course blocks for the specified course, updating the cache if needed. Gets the course blocks for the specified course, updating the cache if needed.
""" """
if kwargs.get('with_storage'): if kwargs.get('with_storage'):
enable_for_current_request(STORAGE_BACKING_FOR_CACHE) waffle().override_for_request(STORAGE_BACKING_FOR_CACHE)
_call_and_retry_if_needed(self, api.get_course_in_cache, **kwargs) _call_and_retry_if_needed(self, api.get_course_in_cache, **kwargs)
......
...@@ -7,11 +7,9 @@ from xmodule.modulestore.exceptions import ItemNotFoundError ...@@ -7,11 +7,9 @@ from xmodule.modulestore.exceptions import ItemNotFoundError
from uuid import uuid4 from uuid import uuid4
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
from openedx.core.djangolib.testing.waffle_utils import override_switch
from ..api import get_cache from ..api import get_cache
from ..block_structure import BlockStructureBlockData from ..block_structure import BlockStructureBlockData
from ..config import _bs_waffle_switch_name
from ..exceptions import BlockStructureNotFound from ..exceptions import BlockStructureNotFound
from ..models import BlockStructureModel from ..models import BlockStructureModel
from ..store import BlockStructureStore from ..store import BlockStructureStore
...@@ -43,18 +41,6 @@ def is_course_in_block_structure_storage(course_key, store): ...@@ -43,18 +41,6 @@ def is_course_in_block_structure_storage(course_key, store):
return False return False
class override_config_setting(override_switch): # pylint:disable=invalid-name
"""
Subclasses override_switch to use the block structure
name-spaced switch names.
"""
def __init__(self, name, active):
super(override_config_setting, self).__init__(
_bs_waffle_switch_name(name),
active
)
class MockXBlock(object): class MockXBlock(object):
""" """
A mock XBlock to be used in unit tests, thereby decoupling the A mock XBlock to be used in unit tests, thereby decoupling the
......
...@@ -6,7 +6,7 @@ from nose.plugins.attrib import attr ...@@ -6,7 +6,7 @@ from nose.plugins.attrib import attr
from unittest import TestCase from unittest import TestCase
from ..block_structure import BlockStructureBlockData from ..block_structure import BlockStructureBlockData
from ..config import RAISE_ERROR_WHEN_NOT_FOUND, STORAGE_BACKING_FOR_CACHE from ..config import RAISE_ERROR_WHEN_NOT_FOUND, STORAGE_BACKING_FOR_CACHE, waffle
from ..exceptions import UsageKeyNotInBlockStructure, BlockStructureNotFound from ..exceptions import UsageKeyNotInBlockStructure, BlockStructureNotFound
from ..manager import BlockStructureManager from ..manager import BlockStructureManager
from ..transformers import BlockStructureTransformers from ..transformers import BlockStructureTransformers
...@@ -14,7 +14,6 @@ from .helpers import ( ...@@ -14,7 +14,6 @@ from .helpers import (
MockModulestoreFactory, MockCache, MockTransformer, MockModulestoreFactory, MockCache, MockTransformer,
ChildrenMapTestMixin, UsageKeyFactoryMixin, ChildrenMapTestMixin, UsageKeyFactoryMixin,
mock_registered_transformers, mock_registered_transformers,
override_config_setting,
) )
...@@ -173,14 +172,14 @@ class TestBlockStructureManager(UsageKeyFactoryMixin, ChildrenMapTestMixin, Test ...@@ -173,14 +172,14 @@ class TestBlockStructureManager(UsageKeyFactoryMixin, ChildrenMapTestMixin, Test
self.assertEquals(TestTransformer1.collect_call_count, 1) self.assertEquals(TestTransformer1.collect_call_count, 1)
def test_get_collected_error_raised(self): def test_get_collected_error_raised(self):
with override_config_setting(RAISE_ERROR_WHEN_NOT_FOUND, active=True): with waffle().override(RAISE_ERROR_WHEN_NOT_FOUND, active=True):
with mock_registered_transformers(self.registered_transformers): with mock_registered_transformers(self.registered_transformers):
with self.assertRaises(BlockStructureNotFound): with self.assertRaises(BlockStructureNotFound):
self.bs_manager.get_collected() self.bs_manager.get_collected()
@ddt.data(True, False) @ddt.data(True, False)
def test_update_collected_if_needed(self, with_storage_backing): def test_update_collected_if_needed(self, with_storage_backing):
with override_config_setting(STORAGE_BACKING_FOR_CACHE, active=with_storage_backing): with waffle().override(STORAGE_BACKING_FOR_CACHE, active=with_storage_backing):
with mock_registered_transformers(self.registered_transformers): with mock_registered_transformers(self.registered_transformers):
self.assertEquals(TestTransformer1.collect_call_count, 0) self.assertEquals(TestTransformer1.collect_call_count, 0)
......
...@@ -12,10 +12,9 @@ from uuid import uuid4 ...@@ -12,10 +12,9 @@ from uuid import uuid4
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
from ..config import PRUNE_OLD_VERSIONS from ..config import PRUNE_OLD_VERSIONS, waffle
from ..exceptions import BlockStructureNotFound from ..exceptions import BlockStructureNotFound
from ..models import BlockStructureModel, _directory_name, _storage_error_handling from ..models import BlockStructureModel, _directory_name, _storage_error_handling
from .helpers import override_config_setting
@ddt.ddt @ddt.ddt
...@@ -31,7 +30,7 @@ class BlockStructureModelTestCase(TestCase): ...@@ -31,7 +30,7 @@ class BlockStructureModelTestCase(TestCase):
self.params = self._create_bsm_params() self.params = self._create_bsm_params()
def tearDown(self): def tearDown(self):
with override_config_setting(PRUNE_OLD_VERSIONS, active=True): with waffle().override(PRUNE_OLD_VERSIONS, active=True):
BlockStructureModel._prune_files(self.usage_key, num_to_keep=0) BlockStructureModel._prune_files(self.usage_key, num_to_keep=0)
super(BlockStructureModelTestCase, self).tearDown() super(BlockStructureModelTestCase, self).tearDown()
...@@ -105,24 +104,24 @@ class BlockStructureModelTestCase(TestCase): ...@@ -105,24 +104,24 @@ class BlockStructureModelTestCase(TestCase):
# old files not pruned # old files not pruned
self._assert_file_count_equal(2) self._assert_file_count_equal(2)
@override_config_setting(PRUNE_OLD_VERSIONS, active=True)
@patch('openedx.core.djangoapps.content.block_structure.config.num_versions_to_keep', Mock(return_value=1)) @patch('openedx.core.djangoapps.content.block_structure.config.num_versions_to_keep', Mock(return_value=1))
def test_prune_files(self): def test_prune_files(self):
self._verify_update_or_create_call('test data', expect_created=True) with waffle().override(PRUNE_OLD_VERSIONS, active=True):
self._verify_update_or_create_call('updated data', expect_created=False) self._verify_update_or_create_call('test data', expect_created=True)
self._assert_file_count_equal(1) self._verify_update_or_create_call('updated data', expect_created=False)
self._assert_file_count_equal(1)
@override_config_setting(PRUNE_OLD_VERSIONS, active=True)
@patch('openedx.core.djangoapps.content.block_structure.config.num_versions_to_keep', Mock(return_value=1)) @patch('openedx.core.djangoapps.content.block_structure.config.num_versions_to_keep', Mock(return_value=1))
@patch('openedx.core.djangoapps.content.block_structure.models.BlockStructureModel._delete_files') @patch('openedx.core.djangoapps.content.block_structure.models.BlockStructureModel._delete_files')
@patch('openedx.core.djangoapps.content.block_structure.models.log') @patch('openedx.core.djangoapps.content.block_structure.models.log')
def test_prune_exception(self, mock_log, mock_delete): def test_prune_exception(self, mock_log, mock_delete):
mock_delete.side_effect = Exception with waffle().override(PRUNE_OLD_VERSIONS, active=True):
self._verify_update_or_create_call('test data', expect_created=True) mock_delete.side_effect = Exception
self._verify_update_or_create_call('updated data', expect_created=False) self._verify_update_or_create_call('test data', expect_created=True)
self._verify_update_or_create_call('updated data', expect_created=False)
self.assertIn('BlockStructure: Exception when deleting old files', mock_log.exception.call_args[0][0]) self.assertIn('BlockStructure: Exception when deleting old files', mock_log.exception.call_args[0][0])
self._assert_file_count_equal(2) # old files not pruned self._assert_file_count_equal(2) # old files not pruned
@ddt.data( @ddt.data(
*product( *product(
...@@ -142,7 +141,7 @@ class BlockStructureModelTestCase(TestCase): ...@@ -142,7 +141,7 @@ class BlockStructureModelTestCase(TestCase):
if num_prior_edits: if num_prior_edits:
self._assert_file_count_equal(num_prior_edits) self._assert_file_count_equal(num_prior_edits)
with override_config_setting(PRUNE_OLD_VERSIONS, active=True): with waffle().override(PRUNE_OLD_VERSIONS, active=True):
self._verify_update_or_create_call('data') self._verify_update_or_create_call('data')
self._assert_file_count_equal(min(prune_keep_count, num_prior_edits + 1)) self._assert_file_count_equal(min(prune_keep_count, num_prior_edits + 1))
......
...@@ -10,9 +10,9 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase ...@@ -10,9 +10,9 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from ..api import get_block_structure_manager from ..api import get_block_structure_manager
from ..config import INVALIDATE_CACHE_ON_PUBLISH from ..config import INVALIDATE_CACHE_ON_PUBLISH, waffle
from ..signals import _update_block_structure_on_course_publish from ..signals import _update_block_structure_on_course_publish
from .helpers import is_course_in_block_structure_cache, override_config_setting from .helpers import is_course_in_block_structure_cache
@ddt.ddt @ddt.ddt
...@@ -54,7 +54,7 @@ class CourseBlocksSignalTest(ModuleStoreTestCase): ...@@ -54,7 +54,7 @@ class CourseBlocksSignalTest(ModuleStoreTestCase):
def test_cache_invalidation(self, invalidate_cache_enabled, mock_bs_manager_clear): def test_cache_invalidation(self, invalidate_cache_enabled, mock_bs_manager_clear):
test_display_name = "Jedi 101" test_display_name = "Jedi 101"
with override_config_setting(INVALIDATE_CACHE_ON_PUBLISH, active=invalidate_cache_enabled): with waffle().override(INVALIDATE_CACHE_ON_PUBLISH, active=invalidate_cache_enabled):
self.course.display_name = test_display_name self.course.display_name = test_display_name
self.store.update_item(self.course, self.user.id) self.store.update_item(self.course, self.user.id)
......
...@@ -6,11 +6,11 @@ from nose.plugins.attrib import attr ...@@ -6,11 +6,11 @@ from nose.plugins.attrib import attr
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from ..config import STORAGE_BACKING_FOR_CACHE from ..config import STORAGE_BACKING_FOR_CACHE, waffle
from ..config.models import BlockStructureConfiguration from ..config.models import BlockStructureConfiguration
from ..exceptions import BlockStructureNotFound from ..exceptions import BlockStructureNotFound
from ..store import BlockStructureStore from ..store import BlockStructureStore
from .helpers import ChildrenMapTestMixin, UsageKeyFactoryMixin, MockCache, MockTransformer, override_config_setting from .helpers import ChildrenMapTestMixin, UsageKeyFactoryMixin, MockCache, MockTransformer
@attr(shard=2) @attr(shard=2)
...@@ -47,13 +47,13 @@ class TestBlockStructureStore(UsageKeyFactoryMixin, ChildrenMapTestMixin, CacheI ...@@ -47,13 +47,13 @@ class TestBlockStructureStore(UsageKeyFactoryMixin, ChildrenMapTestMixin, CacheI
@ddt.data(True, False) @ddt.data(True, False)
def test_get_none(self, with_storage_backing): def test_get_none(self, with_storage_backing):
with override_config_setting(STORAGE_BACKING_FOR_CACHE, active=with_storage_backing): with waffle().override(STORAGE_BACKING_FOR_CACHE, active=with_storage_backing):
with self.assertRaises(BlockStructureNotFound): with self.assertRaises(BlockStructureNotFound):
self.store.get(self.block_structure.root_block_usage_key) self.store.get(self.block_structure.root_block_usage_key)
@ddt.data(True, False) @ddt.data(True, False)
def test_add_and_get(self, with_storage_backing): def test_add_and_get(self, with_storage_backing):
with override_config_setting(STORAGE_BACKING_FOR_CACHE, active=with_storage_backing): with waffle().override(STORAGE_BACKING_FOR_CACHE, active=with_storage_backing):
self.store.add(self.block_structure) self.store.add(self.block_structure)
stored_value = self.store.get(self.block_structure.root_block_usage_key) stored_value = self.store.get(self.block_structure.root_block_usage_key)
self.assertIsNotNone(stored_value) self.assertIsNotNone(stored_value)
...@@ -61,7 +61,7 @@ class TestBlockStructureStore(UsageKeyFactoryMixin, ChildrenMapTestMixin, CacheI ...@@ -61,7 +61,7 @@ class TestBlockStructureStore(UsageKeyFactoryMixin, ChildrenMapTestMixin, CacheI
@ddt.data(True, False) @ddt.data(True, False)
def test_delete(self, with_storage_backing): def test_delete(self, with_storage_backing):
with override_config_setting(STORAGE_BACKING_FOR_CACHE, active=with_storage_backing): with waffle().override(STORAGE_BACKING_FOR_CACHE, active=with_storage_backing):
self.store.add(self.block_structure) self.store.add(self.block_structure)
self.store.delete(self.block_structure.root_block_usage_key) self.store.delete(self.block_structure.root_block_usage_key)
with self.assertRaises(BlockStructureNotFound): with self.assertRaises(BlockStructureNotFound):
...@@ -74,7 +74,7 @@ class TestBlockStructureStore(UsageKeyFactoryMixin, ChildrenMapTestMixin, CacheI ...@@ -74,7 +74,7 @@ class TestBlockStructureStore(UsageKeyFactoryMixin, ChildrenMapTestMixin, CacheI
self.store.get(self.block_structure.root_block_usage_key) self.store.get(self.block_structure.root_block_usage_key)
def test_uncached_with_storage(self): def test_uncached_with_storage(self):
with override_config_setting(STORAGE_BACKING_FOR_CACHE, active=True): with waffle().override(STORAGE_BACKING_FOR_CACHE, active=True):
self.store.add(self.block_structure) self.store.add(self.block_structure)
self.mock_cache.map.clear() self.mock_cache.map.clear()
stored_value = self.store.get(self.block_structure.root_block_usage_key) stored_value = self.store.get(self.block_structure.root_block_usage_key)
......
"""
Test utilities when using waffle.
"""
from waffle.testutils import override_switch as waffle_override_switch
from request_cache.middleware import RequestCache, func_call_cache_key
from ..waffle_utils import is_switch_enabled
class override_switch(waffle_override_switch): # pylint:disable=invalid-name
"""
Subclasses waffle's override_switch in order clear the cache
used on the is_switch_enabled function.
"""
def _clear_cache(self):
"""
Clears the requestcached values on the is_switch_enabled function.
"""
cache_key = func_call_cache_key(is_switch_enabled.request_cached_contained_func, self.name)
RequestCache.get_request_cache().data.pop(cache_key, None)
def __enter__(self):
self._clear_cache()
super(override_switch, self).__enter__()
def __exit__(self, *args, **kwargs):
self._clear_cache()
super(override_switch, self).__exit__(*args, **kwargs)
""" """
Utilities for waffle usage. Utilities for waffle usage.
""" """
from request_cache.middleware import request_cached from abc import ABCMeta
from contextlib import contextmanager
import logging
from waffle.models import Switch
from waffle.utils import get_setting as waffle_setting
from waffle import switch_is_active from waffle import switch_is_active
from request_cache import get_cache as get_request_cache
log = logging.getLogger(__name__)
@request_cached class WafflePlus(object):
def is_switch_enabled(waffle_name):
""" """
Returns and caches whether the given waffle switch is enabled. Waffle helper class that provides native support for
See testing.waffle_utils.override_config_setting for a namespacing waffle settings and caching within a request.
helper to override and clear the cache during tests.
""" """
return switch_is_active(waffle_name) __metaclass__ = ABCMeta
def __init__(self, namespace, log_prefix=None):
self.namespace = namespace
self.log_prefix = log_prefix
def _namespaced_setting_name(self, setting_name):
"""
Returns the namespaced name of the waffle switch/flag.
"""
assert self.namespace is not None
return u'{}.{}'.format(self.namespace, setting_name)
@staticmethod
def _get_request_cache():
"""
Returns the request cache used by WafflePlus classes.
"""
return get_request_cache('WafflePlus')
class WaffleSwitchPlus(WafflePlus):
"""
Waffle Switch helper class that provides native support for
namespacing waffle switches and caching within a request.
"""
def is_enabled(self, switch_name):
"""
Returns and caches whether the given waffle switch is enabled.
"""
namespaced_switch_name = self._namespaced_setting_name(switch_name)
value = self._cached_switches.get(namespaced_switch_name)
if value is None:
value = switch_is_active(namespaced_switch_name)
self._cached_switches[namespaced_switch_name] = value
return value
@contextmanager
def override(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 request cache, not in the model.
"""
previous_active = self.is_enabled(switch_name)
try:
self.override_for_request(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_setting_name(switch_name)
self._cached_switches[namespaced_switch_name] = active
log.info(u"%sSwitch '%s' set to %s for request.", self.log_prefix, namespaced_switch_name, active)
@property
def _cached_switches(self):
"""
Returns cached active values of all switches in this namespace.
"""
return self._all_cached_switches.setdefault(self.namespace, {})
@property
def _all_cached_switches(self):
"""
Returns dictionary of all switches in the request cache,
keyed by namespace.
"""
return self._get_request_cache().setdefault('switches', {})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment