Commit ca124354 by Peter Fogg

Add error handling for elastic search.

parent 4d0a1275
...@@ -16,6 +16,11 @@ class AlreadyOnTeamInCourse(TeamAPIRequestError): ...@@ -16,6 +16,11 @@ class AlreadyOnTeamInCourse(TeamAPIRequestError):
pass pass
class ElasticSearchConnectionError(TeamAPIRequestError):
"""The system was unable to connect to the configured elasticsearch instance."""
pass
class ImmutableMembershipFieldException(Exception): class ImmutableMembershipFieldException(Exception):
"""An attempt was made to change an immutable field on a CourseTeamMembership model""" """An attempt was made to change an immutable field on a CourseTeamMembership model."""
pass pass
""" Search index used to load data into elasticsearch""" """ Search index used to load data into elasticsearch"""
import logging
from elasticsearch.exceptions import ConnectionError
from django.conf import settings from django.conf import settings
from django.db.models.signals import post_delete, post_save from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver from django.dispatch import receiver
...@@ -8,6 +11,7 @@ from functools import wraps ...@@ -8,6 +11,7 @@ from functools import wraps
from search.search_engine_base import SearchEngine from search.search_engine_base import SearchEngine
from .errors import ElasticSearchConnectionError
from .serializers import CourseTeamSerializer, CourseTeam from .serializers import CourseTeamSerializer, CourseTeam
...@@ -103,8 +107,11 @@ class CourseTeamIndexer(object): ...@@ -103,8 +107,11 @@ class CourseTeamIndexer(object):
""" """
Return course team search engine (if feature is enabled). Return course team search engine (if feature is enabled).
""" """
if cls.search_is_enabled(): try:
return SearchEngine.get_search_engine(index=cls.INDEX_NAME) return SearchEngine.get_search_engine(index=cls.INDEX_NAME)
except ConnectionError as err:
logging.error('Error connecting to elasticsearch: %s', err)
raise ElasticSearchConnectionError
@classmethod @classmethod
def search_is_enabled(cls): def search_is_enabled(cls):
...@@ -119,7 +126,10 @@ def course_team_post_save_callback(**kwargs): ...@@ -119,7 +126,10 @@ def course_team_post_save_callback(**kwargs):
""" """
Reindex object after save. Reindex object after save.
""" """
CourseTeamIndexer.index(kwargs['instance']) try:
CourseTeamIndexer.index(kwargs['instance'])
except ElasticSearchConnectionError:
pass
@receiver(post_delete, sender=CourseTeam, dispatch_uid='teams.signals.course_team_post_delete_callback') @receiver(post_delete, sender=CourseTeam, dispatch_uid='teams.signals.course_team_post_delete_callback')
...@@ -127,4 +137,7 @@ def course_team_post_delete_callback(**kwargs): # pylint: disable=invalid-name ...@@ -127,4 +137,7 @@ def course_team_post_delete_callback(**kwargs): # pylint: disable=invalid-name
""" """
Reindex object after delete. Reindex object after delete.
""" """
CourseTeamIndexer.remove(kwargs['instance']) try:
CourseTeamIndexer.remove(kwargs['instance'])
except ElasticSearchConnectionError:
pass
...@@ -5,6 +5,9 @@ import pytz ...@@ -5,6 +5,9 @@ import pytz
from datetime import datetime from datetime import datetime
from dateutil import parser from dateutil import parser
import ddt import ddt
from elasticsearch.exceptions import ConnectionError
from mock import patch
from search.search_engine_base import SearchEngine
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.conf import settings from django.conf import settings
...@@ -1397,3 +1400,49 @@ class TestDeleteMembershipAPI(EventTestMixin, TeamAPITestCase): ...@@ -1397,3 +1400,49 @@ class TestDeleteMembershipAPI(EventTestMixin, TeamAPITestCase):
def test_missing_membership(self): def test_missing_membership(self):
self.delete_membership(self.wind_team.team_id, self.users['student_enrolled'].username, 404) self.delete_membership(self.wind_team.team_id, self.users['student_enrolled'].username, 404)
class TestElasticSearchErrors(TeamAPITestCase):
"""Test that the Team API is robust to Elasticsearch connection errors."""
ES_ERROR = ConnectionError('N/A', 'connection error', {})
@patch.object(SearchEngine, 'get_search_engine', side_effect=ES_ERROR)
def test_list_teams(self, __):
"""Test that text searches return a 503 when Elasticsearch is down.
The endpoint should still return 200 when a search is not supplied."""
self.get_teams_list(
expected_status=503,
data={'course_id': self.test_course_1.id, 'text_search': 'zoinks'},
user='staff'
)
self.get_teams_list(
expected_status=200,
data={'course_id': self.test_course_1.id},
user='staff'
)
@patch.object(SearchEngine, 'get_search_engine', side_effect=ES_ERROR)
def test_create_team(self, __):
"""Test that team creation is robust to Elasticsearch errors."""
self.post_create_team(
expected_status=200,
data=self.build_team_data(name='zoinks'),
user='staff'
)
@patch.object(SearchEngine, 'get_search_engine', side_effect=ES_ERROR)
def test_delete_team(self, __):
"""Test that team deletion is robust to Elasticsearch errors."""
self.delete_team(self.wind_team.team_id, 204, user='staff')
@patch.object(SearchEngine, 'get_search_engine', side_effect=ES_ERROR)
def test_patch_team(self, __):
"""Test that team updates are robust to Elasticsearch errors."""
self.patch_team_detail(
self.wind_team.team_id,
200,
data={'description': 'new description'},
user='staff'
)
...@@ -58,7 +58,7 @@ from .serializers import ( ...@@ -58,7 +58,7 @@ from .serializers import (
add_team_count add_team_count
) )
from .search_indexes import CourseTeamIndexer from .search_indexes import CourseTeamIndexer
from .errors import AlreadyOnTeamInCourse, NotEnrolledInCourseForTeam from .errors import AlreadyOnTeamInCourse, ElasticSearchConnectionError, NotEnrolledInCourseForTeam
TEAM_MEMBERSHIPS_PER_PAGE = 2 TEAM_MEMBERSHIPS_PER_PAGE = 2
TOPICS_PER_PAGE = 12 TOPICS_PER_PAGE = 12
...@@ -293,6 +293,9 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -293,6 +293,9 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
example, the course_id may not reference a real course or the page example, the course_id may not reference a real course or the page
number may be beyond the last page. number may be beyond the last page.
If the server is unable to connect to Elasticsearch, and
the text_search parameter is supplied, a 503 error is returned.
**Response Values for POST** **Response Values for POST**
Any logged in user who has verified their email address can create Any logged in user who has verified their email address can create
...@@ -366,7 +369,14 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -366,7 +369,14 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
return Response(error, status=status.HTTP_400_BAD_REQUEST) return Response(error, status=status.HTTP_400_BAD_REQUEST)
result_filter.update({'topic_id': topic_id}) result_filter.update({'topic_id': topic_id})
if text_search and CourseTeamIndexer.search_is_enabled(): if text_search and CourseTeamIndexer.search_is_enabled():
search_engine = CourseTeamIndexer.engine() try:
search_engine = CourseTeamIndexer.engine()
except ElasticSearchConnectionError:
return Response(
build_api_error(ugettext_noop('Error connecting to elasticsearch')),
status=status.HTTP_503_SERVICE_UNAVAILABLE
)
result_filter.update({'course_id': course_id_string}) result_filter.update({'course_id': course_id_string})
search_results = search_engine.search( search_results = search_engine.search(
......
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