Skip to content

Commit

Permalink
perf: add caching to service calls initiated from BFF (#600)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamstankiewicz authored Nov 27, 2024
1 parent 106b52b commit efa5fc1
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 44 deletions.
2 changes: 1 addition & 1 deletion enterprise_access/apps/api/v1/tests/test_bff_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def setUp(self):
)
@ddt.unpack
@mock_dashboard_dependencies
def test_dashboard_empty_state(
def test_dashboard_empty_state_with_permissions(
self,
mock_get_enterprise_customers_for_user,
mock_get_subscription_licenses_for_learner,
Expand Down
131 changes: 128 additions & 3 deletions enterprise_access/apps/bffs/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.conf import settings
from edx_django_utils.cache import TieredCache

from enterprise_access.apps.api_client.license_manager_client import LicenseManagerUserApiClient
from enterprise_access.apps.api_client.lms_client import LmsApiClient, LmsUserApiClient
from enterprise_access.cache_utils import versioned_cache_key

Expand All @@ -24,7 +25,23 @@ def enterprise_customer_cache_key(enterprise_customer_slug, enterprise_customer_
return versioned_cache_key('enterprise_customer', enterprise_customer_slug, enterprise_customer_uuid)


def get_and_cache_enterprise_customer_users(request, **kwargs):
def subscription_licenses_cache_key(enterprise_customer_uuid, lms_user_id):
return versioned_cache_key('get_subscription_licenses_for_learner', enterprise_customer_uuid, lms_user_id)


def default_enterprise_enrollment_intentions_learner_status_cache_key(enterprise_customer_uuid, lms_user_id):
return versioned_cache_key(
'get_default_enterprise_enrollment_intentions_learner_status',
enterprise_customer_uuid,
lms_user_id
)


def enterprise_course_enrollments_cache_key(enterprise_customer_uuid, lms_user_id):
return versioned_cache_key('get_enterprise_course_enrollments', enterprise_customer_uuid, lms_user_id)


def get_and_cache_enterprise_customer_users(request, timeout=settings.ENTERPRISE_USER_RECORD_CACHE_TIMEOUT, **kwargs):
"""
Retrieves and caches enterprise learner data.
"""
Expand All @@ -42,13 +59,14 @@ def get_and_cache_enterprise_customer_users(request, **kwargs):
username=username,
**kwargs,
)
TieredCache.set_all_tiers(cache_key, response_payload, settings.LMS_CLIENT_TIMEOUT)
TieredCache.set_all_tiers(cache_key, response_payload, timeout)
return response_payload


def get_and_cache_enterprise_customer(
enterprise_customer_slug=None,
enterprise_customer_uuid=None,
timeout=settings.ENTERPRISE_USER_RECORD_CACHE_TIMEOUT,
):
"""
Retrieves and caches enterprise customer data.
Expand All @@ -70,10 +88,117 @@ def get_and_cache_enterprise_customer(
enterprise_customer_uuid=enterprise_customer_uuid,
enterprise_customer_slug=enterprise_customer_slug,
)
TieredCache.set_all_tiers(cache_key, response_payload, settings.LMS_CLIENT_TIMEOUT)
TieredCache.set_all_tiers(cache_key, response_payload, timeout)
return response_payload


def get_and_cache_subscription_licenses_for_learner(
request,
enterprise_customer_uuid,
timeout=settings.SUBSCRIPTION_LICENSES_LEARNER_CACHE_TIMEOUT,
**kwargs
):
"""
Retrieves and caches subscription licenses for a learner.
"""
cache_key = subscription_licenses_cache_key(enterprise_customer_uuid, request.user.id)
cached_response = TieredCache.get_cached_response(cache_key)
if cached_response.is_found:
logger.info(
f'subscription_licenses cache hit for enterprise_customer_uuid {enterprise_customer_uuid}'
)
return cached_response.value

client = LicenseManagerUserApiClient(request)
response_payload = client.get_subscription_licenses_for_learner(
enterprise_customer_uuid=enterprise_customer_uuid,
**kwargs,
)
TieredCache.set_all_tiers(cache_key, response_payload, timeout)
return response_payload


def get_and_cache_default_enterprise_enrollment_intentions_learner_status(
request,
enterprise_customer_uuid,
timeout=settings.DEFAULT_ENTERPRISE_ENROLLMENT_INTENTIONS_CACHE_TIMEOUT,
):
"""
Retrieves and caches default enterprise enrollment intentions for a learner.
"""
cache_key = default_enterprise_enrollment_intentions_learner_status_cache_key(
enterprise_customer_uuid,
request.user.id,
)
cached_response = TieredCache.get_cached_response(cache_key)
if cached_response.is_found:
logger.info(
f'default_enterprise_enrollment_intentions cache hit '
f'for enterprise_customer_uuid {enterprise_customer_uuid}'
)
return cached_response.value

client = LmsUserApiClient(request)
response_payload = client.get_default_enterprise_enrollment_intentions_learner_status(
enterprise_customer_uuid=enterprise_customer_uuid,
)
TieredCache.set_all_tiers(cache_key, response_payload, timeout)
return response_payload


def get_and_cache_enterprise_course_enrollments(
request,
enterprise_customer_uuid,
timeout=settings.ENTERPRISE_COURSE_ENROLLMENTS_CACHE_TIMEOUT,
**kwargs
):
"""
Retrieves and caches enterprise course enrollments for a learner.
"""
cache_key = enterprise_course_enrollments_cache_key(enterprise_customer_uuid, request.user.id)
cached_response = TieredCache.get_cached_response(cache_key)
if cached_response.is_found:
logger.info(
f'enterprise_course_enrollments cache hit for enterprise_customer_uuid {enterprise_customer_uuid}'
)
return cached_response.value

client = LmsUserApiClient(request)
response_payload = client.get_enterprise_course_enrollments(
enterprise_customer_uuid=enterprise_customer_uuid,
**kwargs,
)
TieredCache.set_all_tiers(cache_key, response_payload, timeout)
return response_payload


def invalidate_default_enterprise_enrollment_intentions_learner_status_cache(enterprise_customer_uuid, lms_user_id):
"""
Invalidates the default enterprise enrollment intentions cache for a learner.
"""
cache_key = default_enterprise_enrollment_intentions_learner_status_cache_key(
enterprise_customer_uuid,
lms_user_id,
)
TieredCache.delete_all_tiers(cache_key)


def invalidate_enterprise_course_enrollments_cache(enterprise_customer_uuid, lms_user_id):
"""
Invalidates the enterprise course enrollments cache for a learner.
"""
cache_key = enterprise_course_enrollments_cache_key(enterprise_customer_uuid, lms_user_id)
TieredCache.delete_all_tiers(cache_key)


def invalidate_subscription_licenses_cache(enterprise_customer_uuid, lms_user_id):
"""
Invalidates the subscription licenses cache for a learner.
"""
cache_key = subscription_licenses_cache_key(enterprise_customer_uuid, lms_user_id)
TieredCache.delete_all_tiers(cache_key)


def _get_active_enterprise_customer(enterprise_customer_users):
"""
Get the active enterprise customer user from the list of enterprise customer users.
Expand Down
5 changes: 0 additions & 5 deletions enterprise_access/apps/bffs/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from rest_framework import status

from enterprise_access.apps.api_client.lms_client import LmsApiClient, LmsUserApiClient
from enterprise_access.apps.bffs import serializers
from enterprise_access.apps.bffs.api import (
get_and_cache_enterprise_customer_users,
Expand Down Expand Up @@ -48,10 +47,6 @@ def __init__(self, request):
self._enterprise_features = {}
self.data = {} # Stores processed data for the response

# API clients
self.lms_api_client = LmsApiClient()
self.lms_user_api_client = LmsUserApiClient(request)

# Initialize common context data
self._initialize_common_context_data()

Expand Down
80 changes: 57 additions & 23 deletions enterprise_access/apps/bffs/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@
import logging

from enterprise_access.apps.api_client.license_manager_client import LicenseManagerUserApiClient
from enterprise_access.apps.api_client.lms_client import LmsApiClient, LmsUserApiClient
from enterprise_access.apps.api_client.lms_client import LmsApiClient
from enterprise_access.apps.bffs.api import (
get_and_cache_default_enterprise_enrollment_intentions_learner_status,
get_and_cache_enterprise_course_enrollments,
get_and_cache_subscription_licenses_for_learner,
invalidate_default_enterprise_enrollment_intentions_learner_status_cache,
invalidate_enterprise_course_enrollments_cache,
invalidate_subscription_licenses_cache
)
from enterprise_access.apps.bffs.context import HandlerContext
from enterprise_access.apps.bffs.mixins import BaseLearnerDataMixin
from enterprise_access.apps.bffs.serializers import EnterpriseCustomerUserSubsidiesSerializer
Expand Down Expand Up @@ -66,8 +74,8 @@ def __init__(self, context):
super().__init__(context)

# API Clients
self.license_manager_client = LicenseManagerUserApiClient(self.context.request)
self.lms_user_api_client = LmsUserApiClient(self.context.request)
self.license_manager_user_api_client = LicenseManagerUserApiClient(self.context.request)
self.lms_api_client = LmsApiClient()

def load_and_process(self):
"""
Expand Down Expand Up @@ -174,7 +182,8 @@ def load_subscription_licenses(self):
Load subscription licenses for the learner.
"""
try:
subscriptions_result = self.license_manager_client.get_subscription_licenses_for_learner(
subscriptions_result = get_and_cache_subscription_licenses_for_learner(
request=self.context.request,
enterprise_customer_uuid=self.context.enterprise_customer_uuid,
include_revoked=True,
current_plans_only=False,
Expand Down Expand Up @@ -288,7 +297,14 @@ def check_and_activate_assigned_license(self):
if activation_key:
try:
# Perform side effect: Activate the assigned license
activated_license = self.license_manager_client.activate_license(activation_key)
activated_license = self.license_manager_user_api_client.activate_license(activation_key)

# Invalidate the subscription licenses cache as the cached data changed
# with the now-activated license.
invalidate_subscription_licenses_cache(
enterprise_customer_uuid=self.context.enterprise_customer_uuid,
lms_user_id=self.context.lms_user_id,
)
except Exception as e: # pylint: disable=broad-exception-caught
logger.exception(f"Error activating license {subscription_license.get('uuid')}")
self.add_error(
Expand Down Expand Up @@ -370,16 +386,22 @@ def check_and_auto_apply_license(self):

try:
# Perform side effect: Auto-apply license
auto_applied_license = self.license_manager_client.auto_apply_license(customer_agreement.get('uuid'))
if auto_applied_license:
# Update the context with the auto-applied license data
transformed_auto_applied_licenses = self.transform_subscription_licenses([auto_applied_license])
licenses = self.subscription_licenses + transformed_auto_applied_licenses
subscription_licenses_by_status['activated'] = transformed_auto_applied_licenses
self.context.data['enterprise_customer_user_subsidies']['subscriptions'].update({
'subscription_licenses': licenses,
'subscription_licenses_by_status': subscription_licenses_by_status,
})
auto_applied_license = self.license_manager_user_api_client.auto_apply_license(
customer_agreement.get('uuid')
)
# Invalidate the subscription licenses cache as the cached data changed with the auto-applied license.
invalidate_subscription_licenses_cache(
enterprise_customer_uuid=self.context.enterprise_customer_uuid,
lms_user_id=self.context.lms_user_id,
)
# Update the context with the auto-applied license data
transformed_auto_applied_licenses = self.transform_subscription_licenses([auto_applied_license])
licenses = self.subscription_licenses + transformed_auto_applied_licenses
subscription_licenses_by_status['activated'] = transformed_auto_applied_licenses
self.context.data['enterprise_customer_user_subsidies']['subscriptions'].update({
'subscription_licenses': licenses,
'subscription_licenses_by_status': subscription_licenses_by_status,
})
except Exception as e: # pylint: disable=broad-exception-caught
logger.exception("Error auto-applying license")
self.add_error(
Expand All @@ -391,12 +413,13 @@ def load_default_enterprise_enrollment_intentions(self):
"""
Load default enterprise course enrollments (stubbed)
"""
client = self.lms_user_api_client
try:
default_enrollment_intentions = client.get_default_enterprise_enrollment_intentions_learner_status(
enterprise_customer_uuid=self.context.enterprise_customer_uuid,
)
self.context.data['default_enterprise_enrollment_intentions'] = default_enrollment_intentions
default_enterprise_enrollment_intentions =\
get_and_cache_default_enterprise_enrollment_intentions_learner_status(
request=self.context.request,
enterprise_customer_uuid=self.context.enterprise_customer_uuid,
)
self.context.data['default_enterprise_enrollment_intentions'] = default_enterprise_enrollment_intentions
except Exception as e: # pylint: disable=broad-exception-caught
logger.exception("Error loading default enterprise courses")
self.add_error(
Expand Down Expand Up @@ -436,9 +459,8 @@ def enroll_in_redeemable_default_enterprise_enrollment_intentions(self):
'is_default_auto_enrollment': True,
})

client = LmsApiClient()
try:
response_payload = client.bulk_enroll_enterprise_learners(
response_payload = self.lms_api_client.bulk_enroll_enterprise_learners(
self.context.enterprise_customer_uuid,
bulk_enrollment_payload,
)
Expand Down Expand Up @@ -466,6 +488,17 @@ def enroll_in_redeemable_default_enterprise_enrollment_intentions(self):
'subscription_license_uuid': license_uuids_by_course_run_key.get(course_run_key),
})

# Invalidate the default enterprise enrollment intentions and enterprise course enrollments cache
# as the previously redeemable enrollment intentions have been processed/enrolled.
invalidate_default_enterprise_enrollment_intentions_learner_status_cache(
enterprise_customer_uuid=self.context.enterprise_customer_uuid,
lms_user_id=self.context.lms_user_id,
)
invalidate_enterprise_course_enrollments_cache(
enterprise_customer_uuid=self.context.enterprise_customer_uuid,
lms_user_id=self.context.lms_user_id,
)


class DashboardHandler(BaseLearnerPortalHandler):
"""
Expand Down Expand Up @@ -501,7 +534,8 @@ def load_enterprise_course_enrollments(self):
list: A list of enterprise course enrollments.
"""
try:
enterprise_course_enrollments = self.lms_user_api_client.get_enterprise_course_enrollments(
enterprise_course_enrollments = get_and_cache_enterprise_course_enrollments(
request=self.context.request,
enterprise_customer_uuid=self.context.enterprise_customer_uuid,
is_active=True,
)
Expand Down
10 changes: 6 additions & 4 deletions enterprise_access/apps/content_metadata/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@

logger = logging.getLogger(__name__)

DEFAULT_CACHE_TIMEOUT = getattr(settings, 'CONTENT_METADATA_CACHE_TIMEOUT', 60 * 5)


def get_and_cache_catalog_content_metadata(enterprise_catalog_uuid, content_keys, timeout=None):
def get_and_cache_catalog_content_metadata(
enterprise_catalog_uuid,
content_keys,
timeout=settings.CONTENT_METADATA_CACHE_TIMEOUT,
):
"""
Returns the metadata corresponding to the requested
``content_keys`` within the provided ``enterprise_catalog_uuid``,
Expand Down Expand Up @@ -70,7 +72,7 @@ def get_and_cache_catalog_content_metadata(enterprise_catalog_uuid, content_keys
cache_key = cache_keys_by_content_key.get(fetched_record.get('key'))
content_metadata_to_cache[cache_key] = fetched_record

cache.set_many(content_metadata_to_cache, timeout or DEFAULT_CACHE_TIMEOUT)
cache.set_many(content_metadata_to_cache, timeout)

# Add to our results list everything we just had to fetch
metadata_results_list.extend(fetched_metadata)
Expand Down
8 changes: 5 additions & 3 deletions enterprise_access/apps/subsidy_access_policy/customer_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@

logger = logging.getLogger(__name__)

DEFAULT_CACHE_TIMEOUT = settings.ENTERPRISE_USER_RECORD_CACHE_TIMEOUT


def get_and_cache_enterprise_learner_record(enterprise_customer_uuid, learner_id, timeout=DEFAULT_CACHE_TIMEOUT):
def get_and_cache_enterprise_learner_record(
enterprise_customer_uuid,
learner_id,
timeout=settings.ENTERPRISE_USER_RECORD_CACHE_TIMEOUT,
):
"""
Fetches the enterprise learner record from the Lms client if it exists.
Uses the `learner_id` and `enterprise_customer_uuid` to determine if
Expand Down
Loading

0 comments on commit efa5fc1

Please sign in to comment.