Skip to content

Commit 81ba2cc

Browse files
authored
Changes (#76)
* Implement caching for homepage data and signals to improve performance - Added caching for categories, recent questions, active questions, slider questions, spam questions, and category question mapping in views. - Introduced a cache invalidation mechanism in signals for questions and answers. - Updated the settings to include cache configuration. - Enhanced template tags to cache total question and answer counts. - Refactored the category image retrieval to utilize caching. * Update caching configuration and improve homepage data retrieval - Changed cache backend to Memcached and added a file cache option in settings. - Increased HOME_CACHE_TIMEOUT to 3600 seconds for better performance. - Refactored homepage data retrieval functions to accept a base queryset, enhancing flexibility and efficiency. - Updated calls to caching functions to utilize the new base queryset parameter. * Refactor caching configuration to support Memcached and local memory fallback - Updated the caching settings to use Memcached if available, otherwise default to local memory for development. - Simplified cache backend configuration by defining a single variable for the default cache settings. * Add make_cache_key function for Memcached-safe cache key generation - Introduced a new helper function, make_cache_key, to create cache keys that comply with Memcached restrictions. - Updated the category image retrieval function to utilize the new caching mechanism, enhancing cache key normalization and safety.
1 parent d9cb7fc commit 81ba2cc

7 files changed

Lines changed: 280 additions & 55 deletions

File tree

forums/settings.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,32 @@
235235
'compressor.filters.css_default.CssAbsoluteFilter',
236236
'compressor.filters.cssmin.CSSMinFilter',
237237
)
238-
"""CACHES = {
239-
'default': {
238+
239+
# Use Memcached when the memcache package is available; otherwise use local memory
240+
# (e.g. for local development without Memcached).
241+
try:
242+
import memcache # noqa: F401
243+
_default_cache = {
244+
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
245+
'LOCATION': 'localhost:11211',
246+
'TIMEOUT': 3600 * 24,
247+
'KEY_PREFIX': 'forums',
248+
}
249+
except ImportError:
250+
_default_cache = {
240251
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
241-
'LOCATION': 'unique-snowflake'
252+
'TIMEOUT': 3600 * 24,
253+
'KEY_PREFIX': 'forums',
242254
}
243-
}"""
255+
256+
CACHES = {
257+
'default': _default_cache,
258+
'file_cache': {
259+
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
260+
'LOCATION': 'topper_cache_table',
261+
'TIMEOUT': 3600 * 24 * 30,
262+
},
263+
}
244264

245265
COMPRESS_ENABLED = True
246266

website/apps.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,56 @@
11
from django.apps import AppConfig
2-
from django.db.models.signals import post_save
2+
from django.db.models.signals import post_save, post_delete
3+
34

45
class WebsiteConfig(AppConfig):
56
name = 'website'
67

78
def ready(self):
8-
from .models import Answer, AnswerComment
9-
from .signals import last_active_signal_from_answer, last_active_signal_from_reply
10-
post_save.connect(last_active_signal_from_answer, sender=Answer, dispatch_uid='trigger_last_active_answer')
11-
post_save.connect(last_active_signal_from_reply, sender=AnswerComment, dispatch_uid='trigger_last_active_reply')
9+
from .models import Question, Answer, AnswerComment
10+
from .signals import (
11+
last_active_signal_from_answer,
12+
last_active_signal_from_reply,
13+
home_cache_invalidator,
14+
)
15+
16+
post_save.connect(
17+
last_active_signal_from_answer,
18+
sender=Answer,
19+
dispatch_uid='trigger_last_active_answer',
20+
)
21+
post_save.connect(
22+
last_active_signal_from_reply,
23+
sender=AnswerComment,
24+
dispatch_uid='trigger_last_active_reply',
25+
)
26+
27+
post_save.connect(
28+
home_cache_invalidator,
29+
sender=Question,
30+
dispatch_uid='home_cache_invalidator_question_save',
31+
)
32+
post_delete.connect(
33+
home_cache_invalidator,
34+
sender=Question,
35+
dispatch_uid='home_cache_invalidator_question_delete',
36+
)
37+
post_save.connect(
38+
home_cache_invalidator,
39+
sender=Answer,
40+
dispatch_uid='home_cache_invalidator_answer_save',
41+
)
42+
post_delete.connect(
43+
home_cache_invalidator,
44+
sender=Answer,
45+
dispatch_uid='home_cache_invalidator_answer_delete',
46+
)
47+
post_save.connect(
48+
home_cache_invalidator,
49+
sender=AnswerComment,
50+
dispatch_uid='home_cache_invalidator_answercomment_save',
51+
)
52+
post_delete.connect(
53+
home_cache_invalidator,
54+
sender=AnswerComment,
55+
dispatch_uid='home_cache_invalidator_answercomment_delete',
56+
)

website/helpers.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
import hashlib
23
from website.models import Question
34
from nltk.corpus import stopwords
45
from nltk.tokenize import word_tokenize
@@ -33,6 +34,45 @@ def get_video_info(path):
3334
return info_m
3435

3536

37+
def make_cache_key(prefix, *parts):
38+
"""
39+
Create a Memcached-safe cache key by hashing if necessary.
40+
41+
Memcached has restrictions on cache keys:
42+
- No spaces or control characters
43+
- Maximum 250 bytes length
44+
45+
This function constructs a key from prefix and parts, and hashes it
46+
if it contains spaces, special characters, or exceeds 200 bytes (safe limit).
47+
48+
Args:
49+
prefix: The key prefix (e.g., 'category_image')
50+
*parts: Variable parts to include in the key (e.g., category name)
51+
52+
Returns:
53+
A normalized cache key string safe for Memcached
54+
"""
55+
# Construct the key string
56+
key_parts = [str(part) for part in parts]
57+
key_string = ':'.join([prefix] + key_parts)
58+
59+
# Check if key needs hashing:
60+
# 1. Contains spaces or control characters
61+
# 2. Exceeds 200 bytes (safe limit, Memcached allows 250)
62+
needs_hashing = (
63+
' ' in key_string or
64+
'\x00' in key_string or
65+
len(key_string.encode('utf-8')) > 200
66+
)
67+
68+
if needs_hashing:
69+
# Hash the entire key and use hex digest (64 chars, always safe)
70+
key_hash = hashlib.sha256(key_string.encode('utf-8')).hexdigest()
71+
return f'{prefix}:{key_hash}'
72+
73+
return key_string
74+
75+
3676
def prettify(string):
3777
string = string.lower()
3878
string = string.replace('-', ' ')

website/signals.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,38 @@
11
from django.utils import timezone
2+
from django.core.cache import cache
3+
4+
5+
HOME_CACHE_KEYS = [
6+
'home:categories',
7+
'home:recent_questions',
8+
'home:active_questions',
9+
'home:slider_questions',
10+
'home:spam_questions',
11+
'home:category_question_map',
12+
'stats:total_questions',
13+
'stats:total_answers',
14+
]
15+
16+
17+
def clear_home_cache():
18+
cache.delete_many(HOME_CACHE_KEYS)
19+
220

321
def last_active_signal_from_answer(sender, instance, created, **kwargs):
422
if created or not created:
523
instance.question.last_active = timezone.now()
624
instance.question.last_post_by = instance.uid
725
instance.question.save()
26+
clear_home_cache()
27+
828

929
def last_active_signal_from_reply(sender, instance, created, **kwargs):
1030
if created or not created:
1131
instance.answer.question.last_active = timezone.now()
1232
instance.answer.question.last_post_by = instance.uid
13-
instance.answer.question.save()
33+
instance.answer.question.save()
34+
clear_home_cache()
35+
36+
37+
def home_cache_invalidator(sender, instance, **kwargs):
38+
clear_home_cache()

website/templatetags/count_tags.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django import template
2+
from django.core.cache import cache
23

34
from website.models import Question, Answer
45

@@ -86,7 +87,13 @@ def div(value, arg=1):
8687

8788
# retriving total number of questions
8889
def total_question_count():
90+
cache_key = 'stats:total_questions'
91+
count = cache.get(cache_key)
92+
if count is not None:
93+
return count
94+
8995
count = Question.objects.filter(status=1).count()
96+
cache.set(cache_key, count, 10)
9097
return count
9198

9299

@@ -95,7 +102,13 @@ def total_question_count():
95102

96103
# retriving total number of answers
97104
def total_answer_count():
105+
cache_key = 'stats:total_answers'
106+
count = cache.get(cache_key)
107+
if count is not None:
108+
return count
109+
98110
count = Answer.objects.filter(question__status=1).count()
111+
cache.set(cache_key, count, 10)
99112
return count
100113

101114

website/templatetags/helpers.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
from django import template
2+
from django.core.cache import cache
23

3-
# from website.models import Question, Answer, Notification
4-
from website.helpers import prettify
4+
from website.helpers import prettify, make_cache_key
55
from django.conf import settings
66
import os.path
77

88
register = template.Library()
99

1010

1111
def get_category_image(category):
12+
cache_key = make_cache_key('category_image', category)
13+
cached = cache.get(cache_key)
14+
if cached is not None:
15+
return cached
16+
1217
base_path = settings.BASE_DIR + '/static/website/images/'
1318
file_name = category.replace(' ', '-') + '.jpg'
1419
file_path = base_path + file_name
1520
if os.path.isfile(file_path):
16-
return 'website/images/' + file_name
17-
return False
21+
value = 'website/images/' + file_name
22+
else:
23+
value = False
24+
25+
cache.set(cache_key, value, 3600)
26+
return value
1827

1928

2029
register.filter('get_category_image', get_category_image)

0 commit comments

Comments
 (0)