Commit 17210d18 by Don Mitchell

Merge pull request #4078 from edx/dhm/heartbeat

Refactor heartbeat to delegate to the modulestores and sql
parents 0b37c987 3c9b1011
...@@ -15,7 +15,6 @@ from django.test.utils import override_settings ...@@ -15,7 +15,6 @@ from django.test.utils import override_settings
from student.models import CourseEnrollment from student.models import CourseEnrollment
from xmodule.contentstore.django import contentstore, _CONTENTSTORE from xmodule.contentstore.django import contentstore, _CONTENTSTORE
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.modulestore.tests.django_utils import (studio_store_config, from xmodule.modulestore.tests.django_utils import (studio_store_config,
......
"""
Test the heartbeat
"""
from django.test.client import Client
from django.core.urlresolvers import reverse
import json
from django.db.utils import DatabaseError
import mock
from django.test.utils import override_settings
from django.conf import settings
from django.test.testcases import TestCase
from xmodule.modulestore.tests.django_utils import mongo_store_config
TEST_MODULESTORE = mongo_store_config(settings.TEST_ROOT / "data")
@override_settings(MODULESTORE=TEST_MODULESTORE)
class HeartbeatTestCase(TestCase):
"""
Test the heartbeat
"""
def setUp(self):
self.client = Client()
self.heartbeat_url = reverse('heartbeat')
return super(HeartbeatTestCase, self).setUp()
def tearDown(self):
return super(HeartbeatTestCase, self).tearDown()
def test_success(self):
response = self.client.get(self.heartbeat_url)
self.assertEqual(response.status_code, 200)
def test_sql_fail(self):
with mock.patch('heartbeat.views.connection') as mock_connection:
mock_connection.cursor.return_value.execute.side_effect = DatabaseError
response = self.client.get(self.heartbeat_url)
self.assertEqual(response.status_code, 503)
response_dict = json.loads(response.content)
self.assertIn('SQL', response_dict)
def test_mongo_fail(self):
with mock.patch('pymongo.MongoClient.alive', return_value=False):
response = self.client.get(self.heartbeat_url)
self.assertEqual(response.status_code, 503)
import json
from datetime import datetime
from pytz import UTC
from django.http import HttpResponse
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from dogapi import dog_stats_api from dogapi import dog_stats_api
from util.json_request import JsonResponse
from django.db import connection
from django.db.utils import DatabaseError
from xmodule.exceptions import HeartbeatFailure
@dog_stats_api.timed('edxapp.heartbeat') @dog_stats_api.timed('edxapp.heartbeat')
def heartbeat(request): def heartbeat(request):
""" """
Simple view that a loadbalancer can check to verify that the app is up Simple view that a loadbalancer can check to verify that the app is up. Returns a json doc
of service id: status or message. If the status for any service is anything other than True,
it returns HTTP code 503 (Service Unavailable); otherwise, it returns 200.
""" """
output = { # This refactoring merely delegates to the default modulestore (which if it's mixed modulestore will
'date': datetime.now(UTC).isoformat(), # delegate to all configured modulestores) and a quick test of sql. A later refactoring may allow
'courses': [course.location.to_deprecated_string() for course in modulestore().get_courses()], # any service to register itself as participating in the heartbeat. It's important that all implementation
} # do as little as possible but give a sound determination that they are ready.
return HttpResponse(json.dumps(output, indent=4)) try:
output = modulestore().heartbeat()
except HeartbeatFailure as fail:
return JsonResponse({fail.service: unicode(fail)}, status=503)
cursor = connection.cursor()
try:
cursor.execute("SELECT CURRENT_DATE")
cursor.fetchone()
output['SQL'] = True
except DatabaseError as fail:
return JsonResponse({'SQL': unicode(fail)}, status=503)
return JsonResponse(output)
...@@ -38,3 +38,20 @@ class UndefinedContext(Exception): ...@@ -38,3 +38,20 @@ class UndefinedContext(Exception):
Tried to access an xmodule field which needs a different context (runtime) to have a value. Tried to access an xmodule field which needs a different context (runtime) to have a value.
""" """
pass pass
class HeartbeatFailure(Exception):
"""
Raised when heartbeat fails.
"""
def __unicode__(self, *args, **kwargs):
return self.message
def __init__(self, msg, service):
"""
In addition to a msg, provide the name of the service.
"""
self.service = service
return super(HeartbeatFailure, self).__init__(msg)
...@@ -362,6 +362,12 @@ class ModuleStoreReadBase(ModuleStoreRead): ...@@ -362,6 +362,12 @@ class ModuleStoreReadBase(ModuleStoreRead):
else: else:
return any(c.id == course_id for c in self.get_courses()) return any(c.id == course_id for c in self.get_courses())
def heartbeat(self):
"""
Is this modulestore ready?
"""
# default is to say yes by not raising an exception
return {'default_impl': True}
class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
''' '''
......
...@@ -19,6 +19,7 @@ from opaque_keys.edx.keys import CourseKey, UsageKey ...@@ -19,6 +19,7 @@ from opaque_keys.edx.keys import CourseKey, UsageKey
from xmodule.modulestore.mongo.base import MongoModuleStore from xmodule.modulestore.mongo.base import MongoModuleStore
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
import itertools
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -332,6 +333,18 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -332,6 +333,18 @@ class MixedModuleStore(ModuleStoreWriteBase):
courses.extend(modulestore.get_courses_for_wiki(wiki_slug)) courses.extend(modulestore.get_courses_for_wiki(wiki_slug))
return courses return courses
def heartbeat(self):
"""
Delegate to each modulestore and package the results for the caller.
"""
# could be done in parallel threads if needed
return dict(
itertools.chain.from_iterable(
store.heartbeat().iteritems()
for store in self.modulestores.itervalues()
)
)
def _compare_stores(left, right): def _compare_stores(left, right):
""" """
......
...@@ -38,6 +38,7 @@ from xmodule.modulestore.inheritance import own_metadata, InheritanceMixin, inhe ...@@ -38,6 +38,7 @@ from xmodule.modulestore.inheritance import own_metadata, InheritanceMixin, inhe
from xmodule.tabs import StaticTab, CourseTabList from xmodule.tabs import StaticTab, CourseTabList
from xblock.core import XBlock from xblock.core import XBlock
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.exceptions import HeartbeatFailure
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -1034,3 +1035,12 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -1034,3 +1035,12 @@ class MongoModuleStore(ModuleStoreWriteBase):
field_data = KvsFieldData(kvs) field_data = KvsFieldData(kvs)
return field_data return field_data
def heartbeat(self):
"""
Check that the db is reachable.
"""
if self.database.connection.alive():
return {MONGO_MODULESTORE_TYPE: True}
else:
raise HeartbeatFailure("Can't connect to {}".format(self.database.name), 'mongo')
...@@ -4,6 +4,7 @@ Segregation of pymongo functions from the data modeling mechanisms for split mod ...@@ -4,6 +4,7 @@ Segregation of pymongo functions from the data modeling mechanisms for split mod
import re import re
import pymongo import pymongo
from bson import son from bson import son
from xmodule.exceptions import HeartbeatFailure
class MongoConnection(object): class MongoConnection(object):
""" """
...@@ -41,6 +42,15 @@ class MongoConnection(object): ...@@ -41,6 +42,15 @@ class MongoConnection(object):
self.structures.write_concern = {'w': 1} self.structures.write_concern = {'w': 1}
self.definitions.write_concern = {'w': 1} self.definitions.write_concern = {'w': 1}
def heartbeat(self):
"""
Check that the db is reachable.
"""
if self.database.connection.alive():
return True
else:
raise HeartbeatFailure("Can't connect to {}".format(self.database.name))
def get_structure(self, key): def get_structure(self, key):
""" """
Get the structure from the persistence mechanism whose id is the given key Get the structure from the persistence mechanism whose id is the given key
......
...@@ -1769,3 +1769,9 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1769,3 +1769,9 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
""" """
courses = [] courses = []
return courses return courses
def heartbeat(self):
"""
Check that the db is reachable.
"""
return {SPLIT_MONGO_MODULESTORE_TYPE: self.db_connection.heartbeat()}
...@@ -815,3 +815,13 @@ class XMLModuleStore(ModuleStoreReadBase): ...@@ -815,3 +815,13 @@ class XMLModuleStore(ModuleStoreReadBase):
""" """
courses = self.get_courses() courses = self.get_courses()
return [course.location for course in courses if (course.wiki_slug == wiki_slug)] return [course.location for course in courses if (course.wiki_slug == wiki_slug)]
def heartbeat(self):
"""
Ensure that every known course is loaded and ready to go. Really, just return b/c
if this gets called the __init__ finished which means the courses are loaded.
Returns the course count
"""
return {XML_MODULESTORE_TYPE: True}
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