Commit 8ab8ca2b by Will Daly

Add read replica option to some accessor API calls

parent 0dfd2065
...@@ -33,6 +33,7 @@ htmlcov/ ...@@ -33,6 +33,7 @@ htmlcov/
.cache .cache
nosetests.xml nosetests.xml
coverage.xml coverage.xml
submissions_test_db
# Translations # Translations
*.mo *.mo
......
...@@ -7,5 +7,8 @@ if __name__ == "__main__": ...@@ -7,5 +7,8 @@ if __name__ == "__main__":
if os.environ.get('DJANGO_SETTINGS_MODULE') is None: if os.environ.get('DJANGO_SETTINGS_MODULE') is None:
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
if 'test' in sys.argv[:3]:
sys.argv.append('--noinput')
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
...@@ -8,11 +8,12 @@ TEMPLATE_DEBUG = DEBUG ...@@ -8,11 +8,12 @@ TEMPLATE_DEBUG = DEBUG
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'db', 'TEST_NAME': 'submissions_test_db',
'USER': '', },
'PASSWORD': '',
'HOST': '', 'read_replica': {
'PORT': '', 'ENGINE': 'django.db.backends.sqlite3',
'TEST_MIRROR': 'default'
} }
} }
......
...@@ -6,6 +6,7 @@ import copy ...@@ -6,6 +6,7 @@ import copy
import logging import logging
import json import json
from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.db import IntegrityError, DatabaseError from django.db import IntegrityError, DatabaseError
from dogapi import dog_stats_api from dogapi import dog_stats_api
...@@ -161,12 +162,16 @@ def create_submission(student_item_dict, answer, submitted_at=None, attempt_numb ...@@ -161,12 +162,16 @@ def create_submission(student_item_dict, answer, submitted_at=None, attempt_numb
raise SubmissionInternalError(error_message) raise SubmissionInternalError(error_message)
def get_submission(submission_uuid): def get_submission(submission_uuid, read_replica=False):
"""Retrieves a single submission by uuid. """Retrieves a single submission by uuid.
Args: Args:
submission_uuid (str): Identifier for the submission. submission_uuid (str): Identifier for the submission.
Kwargs:
read_replica (bool): If true, attempt to use the read replica database.
If no read replica is available, use the default database.
Raises: Raises:
SubmissionNotFoundError: Raised if the submission does not exist. SubmissionNotFoundError: Raised if the submission does not exist.
SubmissionRequestError: Raised if the search parameter is not a string. SubmissionRequestError: Raised if the search parameter is not a string.
...@@ -202,7 +207,11 @@ def get_submission(submission_uuid): ...@@ -202,7 +207,11 @@ def get_submission(submission_uuid):
return cached_submission_data return cached_submission_data
try: try:
submission = Submission.objects.get(uuid=submission_uuid) submission_qs = Submission.objects
if read_replica:
submission_qs = _use_read_replica(submission_qs)
submission = submission_qs.get(uuid=submission_uuid)
submission_data = SubmissionSerializer(submission).data submission_data = SubmissionSerializer(submission).data
cache.set(cache_key, submission_data) cache.set(cache_key, submission_data)
except Submission.DoesNotExist: except Submission.DoesNotExist:
...@@ -220,13 +229,17 @@ def get_submission(submission_uuid): ...@@ -220,13 +229,17 @@ def get_submission(submission_uuid):
return submission_data return submission_data
def get_submission_and_student(uuid): def get_submission_and_student(uuid, read_replica=False):
""" """
Retrieve a submission by its unique identifier, including the associated student item. Retrieve a submission by its unique identifier, including the associated student item.
Args: Args:
uuid (str): the unique identifier of the submission. uuid (str): the unique identifier of the submission.
Kwargs:
read_replica (bool): If true, attempt to use the read replica database.
If no read replica is available, use the default database.
Returns: Returns:
Serialized Submission model (dict) containing a serialized StudentItem model Serialized Submission model (dict) containing a serialized StudentItem model
...@@ -237,7 +250,7 @@ def get_submission_and_student(uuid): ...@@ -237,7 +250,7 @@ def get_submission_and_student(uuid):
""" """
# This may raise API exceptions # This may raise API exceptions
submission = get_submission(uuid) submission = get_submission(uuid, read_replica=read_replica)
# Retrieve the student item from the cache # Retrieve the student item from the cache
cache_key = "submissions.student_item.{}".format(submission['student_item']) cache_key = "submissions.student_item.{}".format(submission['student_item'])
...@@ -254,7 +267,11 @@ def get_submission_and_student(uuid): ...@@ -254,7 +267,11 @@ def get_submission_and_student(uuid):
else: else:
# There is probably a more idiomatic way to do this using the Django REST framework # There is probably a more idiomatic way to do this using the Django REST framework
try: try:
student_item = StudentItem.objects.get(id=submission['student_item']) student_item_qs = StudentItem.objects
if read_replica:
student_item_qs = _use_read_replica(student_item_qs)
student_item = student_item_qs.get(id=submission['student_item'])
submission['student_item'] = StudentItemSerializer(student_item).data submission['student_item'] = StudentItemSerializer(student_item).data
cache.set(cache_key, submission['student_item']) cache.set(cache_key, submission['student_item'])
except Exception as ex: except Exception as ex:
...@@ -425,21 +442,30 @@ def get_scores(course_id, student_id): ...@@ -425,21 +442,30 @@ def get_scores(course_id, student_id):
return scores return scores
def get_latest_score_for_submission(submission_uuid): def get_latest_score_for_submission(submission_uuid, read_replica=False):
""" """
Retrieve the latest score for a particular submission. Retrieve the latest score for a particular submission.
Args: Args:
submission_uuid (str): The UUID of the submission to retrieve. submission_uuid (str): The UUID of the submission to retrieve.
Kwargs:
read_replica (bool): If true, attempt to use the read replica database.
If no read replica is available, use the default database.
Returns: Returns:
dict: The serialized score model, or None if no score is available. dict: The serialized score model, or None if no score is available.
""" """
try: try:
score = Score.objects.filter( score_qs = Score.objects.filter(
submission__uuid=submission_uuid submission__uuid=submission_uuid
).order_by("-id").select_related("submission")[0] ).order_by("-id").select_related("submission")
if read_replica:
score_qs = _use_read_replica(score_qs)
score = score_qs[0]
if score.is_hidden(): if score.is_hidden():
return None return None
except IndexError: except IndexError:
...@@ -686,3 +712,21 @@ def _get_or_create_student_item(student_item_dict): ...@@ -686,3 +712,21 @@ def _get_or_create_student_item(student_item_dict):
student_item_dict) student_item_dict)
logger.exception(error_message) logger.exception(error_message)
raise SubmissionInternalError(error_message) raise SubmissionInternalError(error_message)
def _use_read_replica(queryset):
"""
Use the read replica if it's available.
Args:
queryset (QuerySet)
Returns:
QuerySet
"""
return (
queryset.using("read_replica")
if "read_replica" in settings.DATABASES
else queryset
)
\ No newline at end of file
"""
Test API calls using the read replica.
"""
import copy
from django.test import TransactionTestCase
from submissions import api as sub_api
class ReadReplicaTest(TransactionTestCase):
""" Test queries that use the read replica. """
STUDENT_ITEM = {
"student_id": "test student",
"course_id": "test course",
"item_id": "test item",
"item_type": "test type"
}
SCORE = {
"points_earned": 3,
"points_possible": 5
}
def setUp(self):
""" Create a submission and score. """
self.submission = sub_api.create_submission(self.STUDENT_ITEM, "test answer")
self.score = sub_api.set_score(
self.submission['uuid'],
self.SCORE["points_earned"],
self.SCORE["points_possible"]
)
def test_get_submission_and_student(self):
retrieved = sub_api.get_submission_and_student(self.submission['uuid'], read_replica=True)
expected = copy.deepcopy(self.submission)
expected['student_item'] = copy.deepcopy(self.STUDENT_ITEM)
self.assertEqual(retrieved, expected)
def test_get_latest_score_for_submission(self):
retrieved = sub_api.get_latest_score_for_submission(self.submission['uuid'], read_replica=True)
self.assertEqual(retrieved['points_possible'], self.SCORE['points_possible'])
self.assertEqual(retrieved['points_earned'], self.SCORE['points_earned'])
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