diff --git a/apps/actioncounters/fields.py b/apps/actioncounters/fields.py index d8cc78e0cdb..c3a718cac8f 100644 --- a/apps/actioncounters/fields.py +++ b/apps/actioncounters/fields.py @@ -122,3 +122,9 @@ def _change_total(self, delta): # race condition. A subsequent save() could clobber concurrent counter # changes. self.total = self.total + delta + + # HACK: Invalidate this object in cache-machine, if the method is available. + if hasattr(m_cls.objects, 'invalidate'): + m_cls.objects.invalidate(self.instance) + + diff --git a/apps/demos/helpers.py b/apps/demos/helpers.py index 4ea9f58f535..1407a3fe2b7 100644 --- a/apps/demos/helpers.py +++ b/apps/demos/helpers.py @@ -1,4 +1,5 @@ import datetime +import functools import hashlib import random @@ -21,18 +22,54 @@ from .models import Submission, TAG_DESCRIPTIONS, DEMO_LICENSES from . import DEMOS_CACHE_NS_KEY -from devmo.helpers import register_cached_inclusion_tag # Monkeypatch threadedcomments URL reverse() to use devmo's from devmo.urlresolvers import reverse threadedcommentstags.reverse = reverse +TEMPLATE_INCLUDE_CACHE_EXPIRES = getattr(settings, + 'TEMPLATE_INCLUDE_CACHE_EXPIRES', 300) + + def new_context(context, **kw): c = dict(context.items()) c.update(kw) return c +# TODO:liberate ? +def register_cached_inclusion_tag(template, key_fn=None, + expires=TEMPLATE_INCLUDE_CACHE_EXPIRES): + """Decorator for inclusion tags with output caching. + + Accepts a string or function to generate a cache key based on the incoming + parameters, along with an expiration time configurable as + INCLUDE_CACHE_EXPIRES or an explicit parameter""" + + if key_fn is None: + key_fn = template + + def decorator(f): + @functools.wraps(f) + def wrapper(*args, **kw): + + if type(key_fn) is str: + cache_key = key_fn + else: + cache_key = key_fn(*args, **kw) + + out = cache.get(cache_key) + if out is None: + context = f(*args, **kw) + t = jingo.env.get_template(template).render(context) + out = jinja2.Markup(t) + cache.set(cache_key, out, expires) + return out + + return register.function(wrapper) + return decorator + + def submission_key(prefix): """Produce a cache key function with a prefix, which generates the rest of the key based on a submission ID and last-modified timestamp.""" diff --git a/apps/devmo/helpers.py b/apps/devmo/helpers.py index 656709471aa..b40d52a34b9 100644 --- a/apps/devmo/helpers.py +++ b/apps/devmo/helpers.py @@ -1,10 +1,9 @@ import datetime -import functools -import httplib import re -import socket +import httplib import urllib import urlparse +import socket from django.conf import settings from django.core.cache import cache @@ -12,7 +11,7 @@ from django.utils.html import strip_tags import bleach -from jingo import register, env +from jingo import register import jinja2 import pytz from soapbox.models import Message @@ -30,10 +29,6 @@ register.filter(utils.entity_decode) -TEMPLATE_INCLUDE_CACHE_EXPIRES = getattr(settings, - 'TEMPLATE_INCLUDE_CACHE_EXPIRES', 300) - - @register.function def page_title(title): return u'%s | MDN' % title @@ -151,35 +146,3 @@ def get_soapbox_messages(url): @register.inclusion_tag('devmo/elements/soapbox_messages.html') def soapbox_messages(soapbox_messages): return {'soapbox_messages': soapbox_messages} - - -def register_cached_inclusion_tag(template, key_fn=None, - expires=TEMPLATE_INCLUDE_CACHE_EXPIRES): - """Decorator for inclusion tags with output caching. - - Accepts a string or function to generate a cache key based on the incoming - parameters, along with an expiration time configurable as - INCLUDE_CACHE_EXPIRES or an explicit parameter""" - - if key_fn is None: - key_fn = template - - def decorator(f): - @functools.wraps(f) - def wrapper(*args, **kw): - - if isinstance(key_fn, basestring): - cache_key = key_fn - else: - cache_key = key_fn(*args, **kw) - - out = cache.get(cache_key) - if out is None: - context = f(*args, **kw) - t = env.get_template(template).render(context) - out = jinja2.Markup(t) - cache.set(cache_key, out, expires) - return out - - return register.function(wrapper) - return decorator diff --git a/apps/devmo/models.py b/apps/devmo/models.py index 4913ec53271..ab86995a921 100644 --- a/apps/devmo/models.py +++ b/apps/devmo/models.py @@ -14,6 +14,7 @@ from django.db import models from django.utils.functional import cached_property +import caching.base import constance.config import xml.sax from xml.sax.handler import ContentHandler @@ -37,7 +38,16 @@ 'DEFAULT_AVATAR', settings.MEDIA_URL + 'img/avatar-default.png') -class UserProfile(models.Model): +class ModelBase(caching.base.CachingMixin, models.Model): + """Common base model for all MDN models: Implements caching.""" + + objects = caching.base.CachingManager() + + class Meta: + abstract = True + + +class UserProfile(ModelBase): """ The UserProfile *must* exist for each django.contrib.auth.models.User object. This may be relaxed @@ -271,7 +281,7 @@ def parse_header_line(header_line): FIELD_MAP[field_name][1] = '' -class Calendar(models.Model): +class Calendar(ModelBase): """The Calendar spreadsheet""" shortname = models.CharField(max_length=255) @@ -357,7 +367,7 @@ def __unicode__(self): return self.shortname -class Event(models.Model): +class Event(ModelBase): """An event""" date = models.DateField() diff --git a/apps/devmo/tests/test_views.py b/apps/devmo/tests/test_views.py index 7e6c8095417..9bce0fb9d7f 100644 --- a/apps/devmo/tests/test_views.py +++ b/apps/devmo/tests/test_views.py @@ -511,7 +511,7 @@ class LoggingTests(test_utils.TestCase): urls = 'devmo.tests.logging_urls' def setUp(self): - self.old_logging = settings.LOGGING.copy() + self.old_logging = settings.LOGGING def tearDown(self): settings.LOGGING = self.old_logging diff --git a/apps/docs/views.py b/apps/docs/views.py index db89f9d85cb..e57f28f652b 100644 --- a/apps/docs/views.py +++ b/apps/docs/views.py @@ -3,9 +3,10 @@ import random from django.conf import settings -from django.http import HttpResponseRedirect +from django.http import (HttpResponseRedirect) from django.shortcuts import render +from caching.base import cached import commonware from dateutil.parser import parse as date_parse from tower import ugettext as _ @@ -29,6 +30,9 @@ def docs(request): if next.startswith('/'): return HttpResponseRedirect(next) + # Doc of the day + dotd = cached(_get_popular_item, 'kuma_docs_dotd', 24*60*60) + # Recent updates active_docs = [] entries = Entry.objects.filter(feed__shortname='mdc-latest') @@ -58,3 +62,10 @@ def docs(request): 'review_flag_docs': review_flag_docs, 'dotd': dotd} return render(request, 'docs/docs.html', data) + + +def _get_popular_item(): + """Get a single, random item off the popular pages list.""" + # MindTouch is gone, and so is popular.json. Returning None for + # historical compatibility. + return None diff --git a/apps/feeder/models.py b/apps/feeder/models.py index 3ad0d34b67b..caa4a99ab4d 100644 --- a/apps/feeder/models.py +++ b/apps/feeder/models.py @@ -1,12 +1,14 @@ from django.db import models -from django.utils.functional import cached_property +import caching.base import jsonpickle from devmo import SECTIONS_TWITTER, SECTIONS_UPDATES +from devmo.models import ModelBase +import utils -class BundleManager(models.Manager): +class BundleManager(caching.base.CachingManager): """Custom manager for bundles.""" def recent_entries(self, bundles): @@ -20,7 +22,7 @@ def recent_entries(self, bundles): feed__bundles__shortname__in=bundles) -class Bundle(models.Model): +class Bundle(ModelBase): """A bundle of several feeds. A feed can be in several (or no) bundles.""" shortname = models.SlugField( @@ -34,7 +36,7 @@ def __unicode__(self): return self.shortname -class Feed(models.Model): +class Feed(ModelBase): """A feed holds the metadata of an RSS feed.""" shortname = models.SlugField( @@ -76,7 +78,7 @@ def delete_old_entries(self): item.delete() -class Entry(models.Model): +class Entry(ModelBase): """An entry is an item representing feed content.""" feed = models.ForeignKey(Feed, related_name='entries') @@ -102,12 +104,12 @@ class Meta: def __unicode__(self): return '%s: %s' % (self.feed.shortname, self.guid) - @cached_property + @utils.cached_property def parsed(self): """Unpickled feed data.""" return jsonpickle.decode(self.raw) - @cached_property + @utils.cached_property def section(self): """The section this entry is associated with.""" try: diff --git a/apps/kpi/models.py b/apps/kpi/models.py index 0521c3166b2..7ed22c0fddb 100644 --- a/apps/kpi/models.py +++ b/apps/kpi/models.py @@ -1,13 +1,14 @@ -from django.db import models from django.db.models import (CharField, DateField, ForeignKey, PositiveIntegerField) +from sumo.models import ModelBase + L10N_METRIC_CODE = 'general wiki:l10n:coverage' KB_L10N_CONTRIBUTORS_METRIC_CODE = 'general wiki:l10n:contributors' -class MetricKind(models.Model): +class MetricKind(ModelBase): """A programmer-readable identifier of a metric, like 'clicks: search'""" code = CharField(max_length=255, unique=True) @@ -15,7 +16,7 @@ def __unicode__(self): return self.code -class Metric(models.Model): +class Metric(ModelBase): """A single numeric measurement aggregated over a span of time. For example, the number of hits to a page during a specific week. diff --git a/apps/landing/helpers.py b/apps/landing/helpers.py index d483110215b..003a4c26b9e 100644 --- a/apps/landing/helpers.py +++ b/apps/landing/helpers.py @@ -10,17 +10,17 @@ from decimal import Decimal from django.utils.formats import number_format + from devmo import SECTIONS, SECTION_USAGE -from devmo.helpers import register_cached_inclusion_tag -@register_cached_inclusion_tag('landing/newsfeed.html') +@register.inclusion_tag('landing/newsfeed.html') def newsfeed(entries, section_headers=False): """Landing page news feed.""" return {'updates': entries, 'section_headers': section_headers} -@register_cached_inclusion_tag('landing/discussions.html') +@register.inclusion_tag('landing/discussions.html') def discussions_feed(entries): """Landing page news feed.""" return {'updates': entries} diff --git a/apps/landing/templates/landing/newsfeed.html b/apps/landing/templates/landing/newsfeed.html index aa47271ec78..8003b60d803 100644 --- a/apps/landing/templates/landing/newsfeed.html +++ b/apps/landing/templates/landing/newsfeed.html @@ -1,3 +1,4 @@ +{% cache updates %} +{% endcache %} diff --git a/apps/landing/templates/sidebar/twitter.html b/apps/landing/templates/sidebar/twitter.html index 6c18a327fd5..a4a78208ad5 100644 --- a/apps/landing/templates/sidebar/twitter.html +++ b/apps/landing/templates/sidebar/twitter.html @@ -1,3 +1,4 @@ +{% cache tweet_qs %}

{{ _('MDN on Twitter') }}

{% if title %}

{{ title }}

{% endif %} @@ -19,3 +20,4 @@

{% endfor %}

+{% endcache %} diff --git a/apps/notifications/events.py b/apps/notifications/events.py index 289215e6fd6..f01aedbc77b 100644 --- a/apps/notifications/events.py +++ b/apps/notifications/events.py @@ -246,7 +246,7 @@ def _watches_belonging_to_user(cls, user_or_email, object_id=None, return Watch.objects.none() # Filter by stuff in the Watch row: - watches = Watch.objects.filter( + watches = Watch.uncached.filter( user_condition, Q(content_type=ContentType.objects.get_for_model(cls.content_type)) if cls.content_type diff --git a/apps/notifications/models.py b/apps/notifications/models.py index 2da7a639b5e..4fb0e7ce6b1 100644 --- a/apps/notifications/models.py +++ b/apps/notifications/models.py @@ -5,7 +5,7 @@ from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType -from sumo.models import LocaleField +from sumo.models import ModelBase, LocaleField from sumo.urlresolvers import reverse @@ -31,7 +31,7 @@ def multi_raw(query, params, models): for model_class in models] -class EventWatch(models.Model): +class EventWatch(ModelBase): """ Allows anyone to watch a specific item for changes. Uses email instead of user ID so anonymous visitors can also watch things eventually. @@ -74,7 +74,7 @@ def get_remove_url(self): return urlparams(url_, email=self.email) -class Watch(models.Model): +class Watch(ModelBase): """Watch events.""" # Key used by an Event to find watches it manages: event_type = models.CharField(max_length=30, db_index=True) @@ -108,7 +108,7 @@ def activate(self): return self -class WatchFilter(models.Model): +class WatchFilter(ModelBase): """Additional key/value pairs that pare down the scope of a watch""" watch = models.ForeignKey(Watch, related_name='filters') name = models.CharField(max_length=20) diff --git a/apps/sumo/models.py b/apps/sumo/models.py index 1fe672a5966..645de468421 100644 --- a/apps/sumo/models.py +++ b/apps/sumo/models.py @@ -1,8 +1,51 @@ from django.conf import settings from django.db import models +import caching.base from south.modelsinspector import add_introspection_rules +# Our apps should subclass ManagerBase instead of models.Manager or +# caching.base.CachingManager directly. +ManagerBase = caching.base.CachingManager + + +class ModelBase(caching.base.CachingMixin, models.Model): + """ + Base class for SUMO models to abstract some common features. + + * Caching. + """ + + objects = ManagerBase() + uncached = models.Manager() + + class Meta: + abstract = True + + def update(self, **kw): + """ + Shortcut for doing an UPDATE on this object. + + If _signal=False is in ``kw`` the post_save signal won't be sent. + """ + signal = kw.pop('_signal', True) + cls = self.__class__ + for k, v in kw.items(): + setattr(self, k, v) + if signal: + # Detect any attribute changes during pre_save and add those to the + # update kwargs. + attrs = dict(self.__dict__) + models.signals.pre_save.send(sender=cls, instance=self) + for k, v in self.__dict__.items(): + if attrs[k] != v: + kw[k] = v + setattr(self, k, v) + cls.objects.filter(pk=self.pk).update(**kw) + if signal: + models.signals.post_save.send(sender=cls, instance=self, + created=False) + class LocaleField(models.CharField): """CharField with locale settings specific to SUMO defaults.""" diff --git a/apps/users/models.py b/apps/users/models.py index 3d3b7c420bb..1ef34052baa 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -15,6 +15,7 @@ from tower import ugettext_lazy as _lazy from countries import COUNTRIES +from sumo.models import ModelBase from sumo.urlresolvers import reverse from devmo.models import UserProfile @@ -22,7 +23,7 @@ SHA1_RE = re.compile('^[a-f0-9]{40}$') -class Profile(models.Model): +class Profile(ModelBase): """Profile model for django users, get it with user.get_profile().""" user = models.OneToOneField(User, primary_key=True, diff --git a/apps/users/views.py b/apps/users/views.py index 68c106c06b5..4e8a8b1b66f 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -267,7 +267,7 @@ def activate(request, activation_key): # Claim anonymous watches belonging to this email claim_watches.delay(account) - # my_questions = Question.objects.filter(creator=account) + # my_questions = Question.uncached.filter(creator=account) # TODO: remove this after dropping unconfirmed questions. # my_questions.update(status=CONFIRMED) return render(request, 'users/activate.html', diff --git a/apps/wiki/fixtures/wiki/documents.json b/apps/wiki/fixtures/wiki/documents.json index a92c51a28c6..4f8d29fb0eb 100644 --- a/apps/wiki/fixtures/wiki/documents.json +++ b/apps/wiki/fixtures/wiki/documents.json @@ -10,8 +10,7 @@ "modified": "2013-06-06 05:54:18", "is_template": false, "html": "

the keyword video only appears in the body of this article\n

", - "slug": "article-title", - "render_max_age": null + "slug": "article-title" } }, { @@ -25,8 +24,7 @@ "modified": "2013-06-06 05:54:18", "is_template": false, "html": "

the keyword video only appears in the body of this article\n

", - "slug": "article-title-2", - "render_max_age": null + "slug": "article-title-2" } }, { @@ -39,8 +37,7 @@ "is_template": false, "modified": "2013-06-06 05:54:18", "html": "", - "slug": "article-title-3", - "render_max_age": null + "slug": "article-title-3" } }, { @@ -55,8 +52,7 @@ "modified": "2013-06-06 05:54:18", "is_template": false, "html": "

ceci n'est pas une pipe\n

mon dieu!\n

", - "slug": "le-title", - "render_max_age": null + "slug": "le-title" } }, { @@ -71,8 +67,7 @@ "current_revision": 22, "is_template": false, "rendered_html": "

audio is in this but the word for tough things will be ignored\n

", - "slug": "lorem-ipsum", - "render_max_age": null + "slug": "lorem-ipsum" } }, { @@ -85,8 +80,7 @@ "is_template": false, "modified": "2013-06-06 05:54:18", "html": "", - "slug": "article-with-revisions", - "render_max_age": null + "slug": "article-with-revisions" } }, { diff --git a/apps/wiki/management/commands/refresh_wiki_caches.py b/apps/wiki/management/commands/refresh_wiki_caches.py index dfacbf5eeb2..e028b186daa 100644 --- a/apps/wiki/management/commands/refresh_wiki_caches.py +++ b/apps/wiki/management/commands/refresh_wiki_caches.py @@ -21,7 +21,7 @@ from django.core.management.base import (BaseCommand, NoArgsCommand, CommandError) -from wiki.models import Document, Revision +from wiki.models import (Document, Revision) PAGE_EXISTS_KEY_TMPL = getattr(settings, 'wiki_page_exists_key_tmpl', @@ -40,6 +40,7 @@ class Command(BaseCommand): ) def handle(self, *args, **options): + base_url = options['baseurl'] if not base_url: from django.contrib.sites.models import Site diff --git a/apps/wiki/models.py b/apps/wiki/models.py index 7fa200ecc08..8a86ed5c662 100644 --- a/apps/wiki/models.py +++ b/apps/wiki/models.py @@ -15,7 +15,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.core import serializers -from django.core.cache import cache +from django.core.cache import get_cache, cache from django.core.exceptions import ValidationError from django.core.urlresolvers import resolve from django.db import models @@ -353,6 +353,9 @@ DEKI_FILE_URL = re.compile(r'@api/deki/files/(?P\d+)/=') KUMA_FILE_URL = re.compile(r'/files/(?P\d+)/.+\..+') +SECONDARY_CACHE_ALIAS = getattr(settings, + 'SECONDARY_CACHE_ALIAS', + 'secondary') URL_REMAPS_CACHE_KEY_TMPL = 'DocumentZoneUrlRemaps:%s' @@ -1842,7 +1845,8 @@ class DocumentZoneManager(models.Manager): def get_url_remaps(self, locale): cache_key = URL_REMAPS_CACHE_KEY_TMPL % locale - remaps = cache.get(cache_key) + s_cache = get_cache(SECONDARY_CACHE_ALIAS) + remaps = s_cache.get(cache_key) if not remaps: qs = (self.filter(document__locale=locale, @@ -1852,7 +1856,7 @@ def get_url_remaps(self, locale): 'original_path': '/docs/%s' % zone.document.slug, 'new_path': '/%s' % zone.url_root } for zone in qs] - cache.set(cache_key, remaps) + s_cache.set(cache_key, remaps) return remaps @@ -1878,7 +1882,8 @@ def save(self, *args, **kwargs): # Invalidate URL remap cache for this zone locale = self.document.locale cache_key = URL_REMAPS_CACHE_KEY_TMPL % locale - cache.delete(cache_key) + s_cache = get_cache(SECONDARY_CACHE_ALIAS) + s_cache.delete(cache_key) class ReviewTag(TagBase): diff --git a/apps/wiki/tests/test_middleware.py b/apps/wiki/tests/test_middleware.py index b3de5e7f576..16e8f861ffb 100644 --- a/apps/wiki/tests/test_middleware.py +++ b/apps/wiki/tests/test_middleware.py @@ -8,7 +8,7 @@ from django.test.client import Client from django.http import Http404 from django.utils.encoding import smart_str -from django.core.cache import cache +from django.core.cache import get_cache import mock from nose import SkipTest @@ -22,7 +22,7 @@ from . import TestCaseBase, FakeResponse -from wiki.models import (Document, Attachment, DocumentZone) +from wiki.models import (Document, Attachment, DocumentZone, SECONDARY_CACHE_ALIAS) from wiki.tests import (doc_rev, document, new_document_data, revision, normalize_html, create_template_test_users) from wiki.views import _version_groups, DOCUMENT_LAST_MODIFIED_CACHE_KEY_TMPL @@ -33,7 +33,9 @@ class DocumentZoneMiddlewareTestCase(TestCaseBase): def setUp(self): super(DocumentZoneMiddlewareTestCase, self).setUp() - cache.clear() + + s_cache = get_cache(SECONDARY_CACHE_ALIAS) + s_cache.clear() self.zone_root = 'ExtraWiki' self.zone_root_content = 'This is the Zone Root' diff --git a/apps/wiki/tests/test_templates.py b/apps/wiki/tests/test_templates.py index 16cf10678d1..e777e8e5965 100644 --- a/apps/wiki/tests/test_templates.py +++ b/apps/wiki/tests/test_templates.py @@ -836,7 +836,7 @@ def test_approve_revision(self, get_current, reviewed_delay): args=[self.document.slug, self.revision.id]) eq_(200, response.status_code) - r = Revision.objects.get(pk=self.revision.id) + r = Revision.uncached.get(pk=self.revision.id) eq_(significance, r.significance) assert r.reviewed assert r.is_approved @@ -863,7 +863,7 @@ def test_reject_revision(self, get_current, delay): 'comment': comment}, args=[self.document.slug, self.revision.id]) eq_(200, response.status_code) - r = Revision.objects.get(pk=self.revision.id) + r = Revision.uncached.get(pk=self.revision.id) assert r.reviewed assert not r.is_approved delay.assert_called_with(r, r.document, comment) @@ -931,7 +931,7 @@ def test_review_translation(self): follow=True) eq_(200, response.status_code) d = Document.objects.get(pk=doc_es.id) - r = Revision.objects.get(pk=rev_es2.id) + r = Revision.uncached.get(pk=rev_es2.id) eq_(d.current_revision, r) assert r.reviewed assert r.is_approved diff --git a/lib/utils.py b/lib/utils.py index 0f15ddcfe85..604f13f393e 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -41,6 +41,53 @@ def wrapper(self, *args, **kwargs): return decorator +def cached_property(*args, **kw): + # Handles invocation as a direct decorator or + # with intermediate keyword arguments. + if args: # @cached_property + return CachedProperty(args[0]) + else: # @cached_property(name=..., writable=...) + return lambda f: CachedProperty(f, **kw) + + +class CachedProperty(object): + """A decorator that converts a function into a lazy property. The +function wrapped is called the first time to retrieve the result +and than that calculated result is used the next time you access +the value:: + +class Foo(object): + +@cached_property +def foo(self): +# calculate something important here +return 42 + +Lifted from werkzeug. +""" + + def __init__(self, func, name=None, doc=None, writable=False): + self.func = func + self.writable = writable + self.__name__ = name or func.__name__ + self.__doc__ = doc or func.__doc__ + + def __get__(self, obj, type=None): + if obj is None: + return self + _missing = object() + value = obj.__dict__.get(self.__name__, _missing) + if value is _missing: + value = self.func(obj) + obj.__dict__[self.__name__] = value + return value + + def __set__(self, obj, value): + if not self.writable: + raise TypeError('read only attribute') + obj.__dict__[self.__name__] = value + + def entity_decode(str): """Turn HTML entities in a string into unicode.""" return htmlparser.unescape(str) diff --git a/puppet/files/vagrant/settings_local.py b/puppet/files/vagrant/settings_local.py index fea917c548c..127c33c0d1b 100644 --- a/puppet/files/vagrant/settings_local.py +++ b/puppet/files/vagrant/settings_local.py @@ -98,7 +98,18 @@ CACHES = { 'default': { - 'BACKEND': 'memcached_hashring.backend.MemcachedHashRingCache', + # HACK: We currently have 'default' memcache disabled in production. + # This reflects that in local dev. + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + #'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + #'LOCATION': [ + # '127.0.0.1:11211', + #], + 'TIMEOUT': 3600, + 'KEY_PREFIX': 'kuma', + }, + 'secondary': { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 'LOCATION': [ '127.0.0.1:11211', ], @@ -107,7 +118,12 @@ } } -CONSTANCE_DATABASE_CACHE_BACKEND = 'default' +# TODO: Switch this to 'default' when main cache issues are resolved +SECONDARY_CACHE_ALIAS = 'secondary' + +# Use IP:PORT pairs separated by semicolons. +CACHE_BACKEND = 'memcached://localhost:11211?timeout=60' +CONSTANCE_DATABASE_CACHE_BACKEND = CACHE_BACKEND # This is used to hash some things in Django. SECRET_KEY = 'jenny8675309' diff --git a/requirements/prod.txt b/requirements/prod.txt index b12388359fa..3cac867fc91 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -16,6 +16,7 @@ jsonpickle==0.3.1 lockfile==0.8 django-dbgettext==0.1 +-e git://github.com/jbalogh/django-cache-machine.git#egg=django-cache-machine -e git://github.com/django-extensions/django-extensions.git#egg=django_extensions -e git://github.com/jbalogh/jingo.git#egg=jingo -e git://github.com/jsocol/jingo-minify.git#egg=jingo-minify diff --git a/scripts/build.sh b/scripts/build.sh index fe64411fbd1..1324f93fecc 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -59,6 +59,7 @@ DATABASES['default']['USER'] = 'hudson' DATABASES['default']['TEST_NAME'] = '$DB' DATABASES['default']['TEST_CHARSET'] = 'utf8' DATABASES['default']['TEST_COLLATION'] = 'utf8_general_ci' +CACHE_BACKEND = 'caching.backends.locmem://' ASYNC_SIGNALS = False diff --git a/settings.py b/settings.py index a8da769f706..cc74b0b9308 100644 --- a/settings.py +++ b/settings.py @@ -21,7 +21,6 @@ ADMINS = ( # ('Your Name', 'your_email@domain.com'), ) -MANAGERS = ADMINS PROTOCOL = 'https://' DOMAIN = 'developer.mozilla.org' @@ -29,6 +28,8 @@ PRODUCTION_URL = SITE_URL USE_X_FORWARDED_HOST = True +MANAGERS = ADMINS + DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. @@ -57,17 +58,30 @@ DEKIWIKI_APIKEY = 'SET IN LOCAL SETTINGS' DEKIWIKI_MOCK = True +# Cache Settings +CACHE_BACKEND = 'locmem://?timeout=86400' +CACHE_PREFIX = 'kuma:' +CACHE_COUNT_TIMEOUT = 60 # seconds + CACHES = { 'default': { - 'BACKEND': 'memcached_hashring.backend.MemcachedHashRingCache', - 'LOCATION': [ - '127.0.0.1:11211', - ], + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'TIMEOUT': 60, 'KEY_PREFIX': 'kuma', }, + # NOTE: The 'secondary' cache should be the same as 'default' in + # settings_local. The only reason it exists is because we had some issues + # with caching, disabled 'default', and wanted to selectively re-enable + # caching on a case-by-case basis to resolve the issue. + 'secondary': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'TIMEOUT': 60, + 'KEY_PREFIX': 'kuma', + } } +SECONDARY_CACHE_ALIAS = 'secondary' + # Addresses email comes from DEFAULT_FROM_EMAIL = 'notifications@developer.mozilla.org' SERVER_EMAIL = 'server-error@developer.mozilla.org' @@ -470,15 +484,23 @@ def lazy_language_deki_map(): FEEDER_TIMEOUT = 6 # in seconds def JINJA_CONFIG(): - config = { - 'extensions': [ - 'tower.template.i18n', - 'jinja2.ext.with_', - 'jinja2.ext.loopcontrols', - 'jinja2.ext.autoescape', - ], - 'finalize': lambda x: x if x is not None else '' - } + import jinja2 + from django.conf import settings + from django.core.cache.backends.memcached import CacheClass as MemcachedCacheClass + from caching.base import cache + config = {'extensions': ['tower.template.i18n', 'caching.ext.cache', + 'jinja2.ext.with_', 'jinja2.ext.loopcontrols', + 'jinja2.ext.autoescape'], + 'finalize': lambda x: x if x is not None else ''} + if isinstance(cache, MemcachedCacheClass) and not settings.DEBUG: + # We're passing the _cache object directly to jinja because + # Django can't store binary directly; it enforces unicode on it. + # Details: http://jinja.pocoo.org/2/documentation/api#bytecode-cache + # and in the errors you get when you try it the other way. + bc = jinja2.MemcachedBytecodeCache(cache._cache, + "%sj2:" % settings.CACHE_PREFIX) + config['cache_size'] = -1 # Never clear the cache + config['bytecode_cache'] = bc return config # Let Tower know about our additional keywords. diff --git a/vendor b/vendor index ada44dd5b7f..8f91262c89e 160000 --- a/vendor +++ b/vendor @@ -1 +1 @@ -Subproject commit ada44dd5b7f43bd99a266f8306f6f95f7612093a +Subproject commit 8f91262c89e41d8c8d9187fd10bda610d8efb46f