diff --git a/contentcuration/automation/utils/appnexus/base.py b/contentcuration/automation/utils/appnexus/base.py index c242593feb..d39ec7f15c 100644 --- a/contentcuration/automation/utils/appnexus/base.py +++ b/contentcuration/automation/utils/appnexus/base.py @@ -190,6 +190,35 @@ def create_backend(self) -> Backend: pass +class CompositeBackend: + def __init__(self, backend, prefixes): + self.backend = backend + self.prefixes = prefixes + self._connected = False + + def connect(self, **kwargs): + """ + Loops through a list of prefixes in order to establish + which backend is available to connect. + + """ + for prefix in self.prefixes: + self.backend.url_prefix = prefix + if self.backend.connect(): + self._connected = True + return True + raise AssertionError( + f"Could not connect to any backend in list: {self.prefixes}" + ) + + def make_request(self, request): + if self._connected: + return self.backend.make_request(request) + else: + self.connect() + return self.backend.make_request(request) + + class Adapter: """ Base class for adapters that interact with a backend interface. diff --git a/contentcuration/contentcuration/tests/utils/test_recommendations.py b/contentcuration/contentcuration/tests/utils/test_recommendations.py index c64e6ef489..4c90e55033 100644 --- a/contentcuration/contentcuration/tests/utils/test_recommendations.py +++ b/contentcuration/contentcuration/tests/utils/test_recommendations.py @@ -4,6 +4,7 @@ from automation.models import RecommendationsCache from automation.utils.appnexus import errors from automation.utils.appnexus.base import BackendResponse +from automation.utils.appnexus.base import CompositeBackend from django.test import TestCase from kolibri_public.models import ContentNode as PublicContentNode from mock import MagicMock @@ -550,6 +551,8 @@ def test_prepare_url_with_none(self): @patch("contentcuration.utils.recommendations.settings") def test_create_backend_with_url_no_scheme(self, mock_settings): + mock_settings.SITE_ID = "production" + mock_settings.PRODUCTION_SITE_ID = "production" mock_settings.CURRICULUM_AUTOMATION_API_URL = "api.example.com" backend = self.factory.create_backend() @@ -559,6 +562,8 @@ def test_create_backend_with_url_no_scheme(self, mock_settings): @patch("contentcuration.utils.recommendations.settings") def test_create_backend_with_url_with_scheme(self, mock_settings): + mock_settings.SITE_ID = "production" + mock_settings.PRODUCTION_SITE_ID = "production" mock_settings.CURRICULUM_AUTOMATION_API_URL = "https://api.example.com" backend = self.factory.create_backend() @@ -568,6 +573,8 @@ def test_create_backend_with_url_with_scheme(self, mock_settings): @patch("contentcuration.utils.recommendations.settings") def test_create_backend_with_empty_url(self, mock_settings): + mock_settings.SITE_ID = "production" + mock_settings.PRODUCTION_SITE_ID = "production" mock_settings.CURRICULUM_AUTOMATION_API_URL = "" backend = self.factory.create_backend() @@ -577,9 +584,21 @@ def test_create_backend_with_empty_url(self, mock_settings): @patch("contentcuration.utils.recommendations.settings") def test_create_backend_with_no_url(self, mock_settings): + mock_settings.SITE_ID = "production" + mock_settings.PRODUCTION_SITE_ID = "production" mock_settings.CURRICULUM_AUTOMATION_API_URL = None backend = self.factory.create_backend() self.assertIsInstance(backend, Recommendations) self.assertEqual(backend.base_url, None) self.assertEqual(backend.connect_endpoint, "/connect") + + @patch("contentcuration.utils.recommendations.settings") + def test_create_backend_to_unstable_url(self, mock_settings): + mock_settings.CURRICULUM_AUTOMATION_API_URL = "http://api.example.com:8080" + mock_settings.SITE_ID = "unstable" + mock_settings.PRODUCTION_SITE_ID = "production" + + backend = self.factory.create_backend() + self.assertIsInstance(backend, CompositeBackend) + self.assertEqual(backend.prefixes, ["unstable", "stable"]) diff --git a/contentcuration/contentcuration/utils/recommendations.py b/contentcuration/contentcuration/utils/recommendations.py index f24e88fe9f..c0559e8009 100644 --- a/contentcuration/contentcuration/utils/recommendations.py +++ b/contentcuration/contentcuration/utils/recommendations.py @@ -16,6 +16,7 @@ from automation.utils.appnexus.base import BackendFactory from automation.utils.appnexus.base import BackendRequest from automation.utils.appnexus.base import BackendResponse +from automation.utils.appnexus.base import CompositeBackend from django.conf import settings from django.db.models import Exists from django.db.models import F @@ -108,10 +109,17 @@ def _prepare_url(self, url): ) def create_backend(self) -> Backend: - backend = Recommendations() - backend.base_url = self._prepare_url(settings.CURRICULUM_AUTOMATION_API_URL) - backend.connect_endpoint = "/connect" - return backend + if settings.SITE_ID == settings.PRODUCTION_SITE_ID: + backend = Recommendations() + backend.base_url = self._prepare_url(settings.CURRICULUM_AUTOMATION_API_URL) + backend.connect_endpoint = "/connect" + return backend + else: + backend = Recommendations() + backend.base_url = self._prepare_url(settings.CURRICULUM_AUTOMATION_API_URL) + backend.connect_endpoint = "/connect" + + return CompositeBackend(backend, ["unstable", "stable"]) class RecommendationsAdapter(Adapter):