-
Notifications
You must be signed in to change notification settings - Fork 16
feat: add support for concurrent API requests to external services in BFF API layer #734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
adamstankiewicz
wants to merge
9
commits into
main
Choose a base branch
from
ags/ent-10429
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 5 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
1641f41
chore: move secured algolia api key request to BaseLearnerPortalHandler
adamstankiewicz 6d9be2d
feat: initial ConcurrentTaskRunner
adamstankiewicz e4c9ac6
chore: clean up
adamstankiewicz 5aa82f5
chore: updates
adamstankiewicz 025e059
chore: re-order methods
adamstankiewicz 9b85132
chore: updates
adamstankiewicz 3eeb579
chore: updates
adamstankiewicz 0c8039a
chore: updates
adamstankiewicz 95f93b9
feat: wrap concurrent requests behind feature flag
adamstankiewicz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,8 @@ | |
| """ | ||
| import json | ||
| import logging | ||
| import time | ||
| from enum import Enum, auto | ||
|
|
||
| from enterprise_access.apps.api_client.constants import LicenseStatuses | ||
| from enterprise_access.apps.api_client.license_manager_client import LicenseManagerUserApiClient | ||
|
|
@@ -15,12 +17,16 @@ | |
| invalidate_subscription_licenses_cache | ||
| ) | ||
| from enterprise_access.apps.bffs.context import HandlerContext | ||
| from enterprise_access.apps.bffs.mixins import BaseLearnerDataMixin, LearnerDashboardDataMixin | ||
| from enterprise_access.apps.bffs.mixins import AlgoliaDataMixin, BaseLearnerDataMixin, LearnerDashboardDataMixin | ||
| from enterprise_access.apps.bffs.serializers import EnterpriseCustomerUserSubsidiesSerializer | ||
| from enterprise_access.apps.bffs.task_runner import ConcurrentTaskRunner | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| MOCK_TASK_DELAY = 5 | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [inform] Temporary constant to use with |
||
|
|
||
|
|
||
| class BaseHandler: | ||
| """ | ||
| A base handler class that provides shared core functionality for different BFF handlers. | ||
|
|
@@ -64,14 +70,20 @@ def add_warning(self, user_message, developer_message): | |
| ) | ||
|
|
||
|
|
||
| class BaseLearnerPortalHandler(BaseHandler, BaseLearnerDataMixin): | ||
| class BaseLearnerPortalHandler(BaseHandler, AlgoliaDataMixin, BaseLearnerDataMixin): | ||
| """ | ||
| A base handler class for learner-focused routes. | ||
|
|
||
| The `BaseLearnerHandler` extends `BaseHandler` and provides shared core functionality | ||
| across all learner-focused page routes, such as the learner dashboard, search, and course routes. | ||
| """ | ||
|
|
||
| class CONCURRENCY_GROUPS(Enum): | ||
| """ | ||
| Group names for concurrent tasks. | ||
| """ | ||
| DEFAULT = auto() | ||
|
|
||
| def __init__(self, context): | ||
| """ | ||
| Initializes the BaseLearnerPortalHandler with a HandlerContext and API clients. | ||
|
|
@@ -84,26 +96,69 @@ def __init__(self, context): | |
| self.license_manager_user_api_client = LicenseManagerUserApiClient(self.context.request) | ||
| self.lms_api_client = LmsApiClient() | ||
|
|
||
| def _get_concurrent_tasks(self): | ||
| """ | ||
| Establishes the data structure for tasks and adds base tasks. | ||
| Subclasses may call this method via super() to extend the tasks | ||
| for any specific group. | ||
| """ | ||
| # Initialize groups | ||
| tasks = { | ||
| self.CONCURRENCY_GROUPS.DEFAULT: [], | ||
| } | ||
|
|
||
| # Add tasks to default group | ||
| tasks[self.CONCURRENCY_GROUPS.DEFAULT].extend([ | ||
| self.load_and_process_subsidies, | ||
| self.load_secured_algolia_api_key, | ||
| self.load_and_process_default_enrollment_intentions, | ||
| ]) | ||
|
|
||
| return tasks | ||
|
|
||
| def load_secured_algolia_api_key(self): | ||
| """ | ||
| Temporary override to add delay. | ||
| """ | ||
| time.sleep(MOCK_TASK_DELAY) | ||
| super().load_secured_algolia_api_key() | ||
|
|
||
| def load_and_process_subsidies(self): | ||
| """ | ||
| Load and process subsidies for learners | ||
| """ | ||
| time.sleep(MOCK_TASK_DELAY) | ||
| empty_subsidies = { | ||
| 'subscriptions': { | ||
| 'customer_agreement': None, | ||
| }, | ||
| } | ||
| self.context.data['enterprise_customer_user_subsidies'] =\ | ||
| EnterpriseCustomerUserSubsidiesSerializer(empty_subsidies).data | ||
|
|
||
| # Retrieve and process subsidies | ||
| self.load_and_process_subscription_licenses() | ||
|
|
||
| def load_and_process_default_enrollment_intentions(self): | ||
| """ | ||
| Helper method to encapsulate the two-step enrollment process | ||
| into a single unit of work for the concurrent runner. | ||
| """ | ||
| time.sleep(MOCK_TASK_DELAY) | ||
| self.load_default_enterprise_enrollment_intentions() | ||
| self.enroll_in_redeemable_default_enterprise_enrollment_intentions() | ||
|
|
||
| def load_and_process(self): | ||
| """ | ||
| Loads and processes data. This is a basic implementation that can be overridden by subclasses. | ||
|
|
||
| The method in this class simply calls common learner logic to ensure the context is set up. | ||
| """ | ||
| try: | ||
| # Verify enterprise customer attrs have learner portal enabled | ||
| # Verify enterprise customer exists and has learner portal enabled | ||
| self.ensure_learner_portal_enabled() | ||
|
|
||
| # Transform enterprise customer data | ||
| self.transform_enterprise_customers() | ||
|
|
||
| # Retrieve and process subscription licenses. Handles activation and auto-apply logic. | ||
| self.load_and_process_subsidies() | ||
|
|
||
| # Retrieve default enterprise courses and enroll in the redeemable ones | ||
| self.load_default_enterprise_enrollment_intentions() | ||
| self.enroll_in_redeemable_default_enterprise_enrollment_intentions() | ||
| except Exception as exc: # pylint: disable=broad-exception-caught | ||
| except Exception as exc: # pylint: disable=broad-except | ||
| logger.exception( | ||
| "Error loading/processing learner portal handler for request user %s and enterprise customer %s", | ||
| self.context.lms_user_id, | ||
|
|
@@ -113,6 +168,26 @@ def load_and_process(self): | |
| user_message="Could not load and/or process common data", | ||
| developer_message=f"Unable to load and/or process common learner portal data: {exc}", | ||
| ) | ||
| return | ||
|
|
||
| all_tasks_to_run = self._get_concurrent_tasks() | ||
| with ConcurrentTaskRunner(task_definitions=all_tasks_to_run) as runner: | ||
| task_results = runner.run_group(self.CONCURRENCY_GROUPS.DEFAULT) | ||
| def handle_task_error(task_name, error_message): | ||
| logger.error( | ||
| "Error running concurrent task '%s' for request user %s and enterprise customer %s: %s", | ||
| task_name, | ||
| self.context.lms_user_id, | ||
| self.context.enterprise_customer_uuid, | ||
| error_message, | ||
| ) | ||
| self.add_error( | ||
| user_message="Could not load and/or process a concurrent task", | ||
| developer_message=( | ||
| f"Unable to load and/or process concurrent task '{task_name}': {error_message}" | ||
| ), | ||
| ) | ||
| runner.handle_failed_tasks(task_results, handle_task_error) | ||
|
|
||
| def ensure_learner_portal_enabled(self): | ||
| """ | ||
|
|
@@ -166,19 +241,6 @@ def transform_enterprise_customers(self): | |
| f"No linked enterprise customer users found in the context for request user {self.context.lms_user_id}" | ||
| ) | ||
|
|
||
| def load_and_process_subsidies(self): | ||
| """ | ||
| Load and process subsidies for learners | ||
| """ | ||
| empty_subsidies = { | ||
| 'subscriptions': { | ||
| 'customer_agreement': None, | ||
| }, | ||
| } | ||
| self.context.data['enterprise_customer_user_subsidies'] =\ | ||
| EnterpriseCustomerUserSubsidiesSerializer(empty_subsidies).data | ||
| self.load_and_process_subscription_licenses() | ||
|
|
||
| def transform_enterprise_customer_user(self, enterprise_customer_user): | ||
| """ | ||
| Transform the enterprise customer user data. | ||
|
|
@@ -698,27 +760,23 @@ class DashboardHandler(LearnerDashboardDataMixin, BaseLearnerPortalHandler): | |
| of data specific to the learner dashboard. | ||
| """ | ||
|
|
||
| def load_and_process(self): | ||
| def _get_concurrent_tasks(self): | ||
| """ | ||
| Loads and processes data for the learner dashboard route. | ||
|
|
||
| This method overrides the `load_and_process` method in `BaseLearnerPortalHandler`. | ||
| This is the key method. It extends the tasks from its parent. | ||
| """ | ||
| super().load_and_process() | ||
| tasks = super()._get_concurrent_tasks() | ||
| tasks[self.CONCURRENCY_GROUPS.DEFAULT].extend([ | ||
| self.load_enterprise_course_enrollments, | ||
| ]) | ||
| return tasks | ||
|
|
||
| try: | ||
| # Load data specific to the dashboard route | ||
| self.load_enterprise_course_enrollments() | ||
| except Exception as e: # pylint: disable=broad-exception-caught | ||
| logger.exception( | ||
| "Error loading and/or processing dashboard data for user %s and enterprise customer %s", | ||
| self.context.lms_user_id, | ||
| self.context.enterprise_customer_uuid, | ||
| ) | ||
| self.add_error( | ||
| user_message="Could not load and/or processing the learner dashboard.", | ||
| developer_message=f"Failed to load and/or processing the learner dashboard data: {e}", | ||
| ) | ||
| def load_enterprise_course_enrollments(self): | ||
| """ | ||
| Temporary override to add delay. | ||
| """ | ||
| time.sleep(MOCK_TASK_DELAY) | ||
| # raise Exception('Failed to load enterprise course enrollments?!') | ||
| return super().load_enterprise_course_enrollments() | ||
|
|
||
|
|
||
| class SearchHandler(BaseLearnerPortalHandler): | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[inform] Moved outside of
context.pyand intoBaseLearnerPortalHandler.