-
Notifications
You must be signed in to change notification settings - Fork 1
[PRMP-1048] Concurrency bulk upload cron #954
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
Open
SWhyteAnswer
wants to merge
4
commits into
main
Choose a base branch
from
PRMP-1048
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.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
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 |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| from services.concurrency_controller_service import ConcurrencyControllerService | ||
| from utils.audit_logging_setup import LoggingService | ||
| from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions | ||
| from utils.decorators.override_error_check import override_error_check | ||
| from utils.decorators.set_audit_arg import set_request_context_for_logging | ||
|
|
||
| logger = LoggingService(__name__) | ||
|
|
||
|
|
||
| def validate_event(event): | ||
| target_function = event.get("targetFunction") | ||
| reserved_concurrency = event.get("reservedConcurrency") | ||
|
|
||
| if not target_function: | ||
| logger.error("Missing required parameter: targetFunction") | ||
| raise ValueError("targetFunction is required") | ||
|
|
||
| if reserved_concurrency is None: | ||
| logger.error("Missing required parameter: reservedConcurrency") | ||
| raise ValueError("reservedConcurrency is required") | ||
|
|
||
| return target_function, reserved_concurrency | ||
|
|
||
|
|
||
| @set_request_context_for_logging | ||
| @override_error_check | ||
| @handle_lambda_exceptions | ||
| def lambda_handler(event, _context): | ||
| target_function, reserved_concurrency = validate_event(event) | ||
|
|
||
| service = ConcurrencyControllerService() | ||
| updated_concurrency = service.update_function_concurrency(target_function, reserved_concurrency) | ||
|
|
||
| return { | ||
| "statusCode": 200, | ||
| "body": { | ||
| "message": "Concurrency updated successfully", | ||
| "function": target_function, | ||
| "reservedConcurrency": updated_concurrency | ||
| } | ||
| } |
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 |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import boto3 | ||
| from botocore.exceptions import ClientError | ||
| from utils.audit_logging_setup import LoggingService | ||
|
|
||
| logger = LoggingService(__name__) | ||
|
|
||
|
|
||
| class ConcurrencyControllerService: | ||
| def __init__(self): | ||
| self.lambda_client = boto3.client("lambda") | ||
|
|
||
| def update_function_concurrency(self, target_function, reserved_concurrency): | ||
| logger.info( | ||
| f"Updating reserved concurrency for function '{target_function}' to {reserved_concurrency}" | ||
| ) | ||
|
|
||
| try: | ||
| response = self.lambda_client.put_function_concurrency( | ||
| FunctionName=target_function, | ||
| ReservedConcurrentExecutions=reserved_concurrency | ||
| ) | ||
|
|
||
| updated_concurrency = response.get("ReservedConcurrentExecutions") | ||
|
|
||
| if updated_concurrency is None: | ||
| logger.error("Response did not contain ReservedConcurrentExecutions") | ||
| raise ValueError("Failed to confirm concurrency update from AWS response") | ||
|
|
||
| if updated_concurrency != reserved_concurrency: | ||
| logger.error( | ||
| f"Concurrency mismatch: requested {reserved_concurrency}, " | ||
| f"AWS returned {updated_concurrency}" | ||
| ) | ||
| raise ValueError("Concurrency update verification failed") | ||
|
|
||
| logger.info( | ||
| f"Successfully updated concurrency for '{target_function}'. " | ||
| f"Reserved concurrency set to: {updated_concurrency}" | ||
| ) | ||
|
|
||
| return updated_concurrency | ||
| except ClientError as e: | ||
| error_code = e.response.get("Error", {}).get("Code", "") | ||
| if error_code == "ResourceNotFoundException": | ||
| logger.error(f"Lambda function '{target_function}' not found") | ||
| else: | ||
| logger.error(f"Failed to update concurrency: {str(e)}") | ||
| raise | ||
217 changes: 217 additions & 0 deletions
217
lambdas/tests/unit/handlers/test_concurrency_controller_handler.py
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 |
|---|---|---|
| @@ -0,0 +1,217 @@ | ||
| import json | ||
| import pytest | ||
| from botocore.exceptions import ClientError | ||
| from handlers.concurrency_controller_handler import lambda_handler, validate_event | ||
| from unittest.mock import MagicMock | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def mock_concurrency_controller_service(mocker): | ||
| mocked_class = mocker.patch( | ||
| "handlers.concurrency_controller_handler.ConcurrencyControllerService" | ||
| ) | ||
| mocked_instance = mocked_class.return_value | ||
| yield mocked_instance | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def mock_logger(mocker): | ||
| return mocker.patch("handlers.concurrency_controller_handler.logger") | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def valid_event(): | ||
| return { | ||
| "targetFunction": "test-lambda-function", | ||
| "reservedConcurrency": 10 | ||
| } | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def event_with_zero_concurrency(): | ||
| return { | ||
| "targetFunction": "test-lambda-function", | ||
| "reservedConcurrency": 0 | ||
| } | ||
|
|
||
|
|
||
| def test_lambda_handler_success(valid_event, context, mock_concurrency_controller_service): | ||
| mock_concurrency_controller_service.update_function_concurrency.return_value = 10 | ||
|
|
||
| result = lambda_handler(valid_event, context) | ||
|
|
||
| mock_concurrency_controller_service.update_function_concurrency.assert_called_once_with( | ||
| "test-lambda-function", 10 | ||
| ) | ||
|
|
||
| assert result["statusCode"] == 200 | ||
| assert result["body"]["message"] == "Concurrency updated successfully" | ||
| assert result["body"]["function"] == "test-lambda-function" | ||
| assert result["body"]["reservedConcurrency"] == 10 | ||
|
|
||
|
|
||
| def test_lambda_handler_with_zero_concurrency( | ||
| event_with_zero_concurrency, context, mock_concurrency_controller_service | ||
| ): | ||
| mock_concurrency_controller_service.update_function_concurrency.return_value = 0 | ||
|
|
||
| result = lambda_handler(event_with_zero_concurrency, context) | ||
|
|
||
| mock_concurrency_controller_service.update_function_concurrency.assert_called_once_with( | ||
| "test-lambda-function", 0 | ||
| ) | ||
|
|
||
| assert result["statusCode"] == 200 | ||
| assert result["body"]["message"] == "Concurrency updated successfully" | ||
| assert result["body"]["function"] == "test-lambda-function" | ||
| assert result["body"]["reservedConcurrency"] == 0 | ||
|
|
||
|
|
||
| def test_lambda_handler_with_large_concurrency(context, mock_concurrency_controller_service): | ||
| event = { | ||
| "targetFunction": "test-lambda-function", | ||
| "reservedConcurrency": 1000 | ||
| } | ||
|
|
||
| mock_concurrency_controller_service.update_function_concurrency.return_value = 1000 | ||
|
|
||
| result = lambda_handler(event, context) | ||
|
|
||
| mock_concurrency_controller_service.update_function_concurrency.assert_called_once_with( | ||
| "test-lambda-function", 1000 | ||
| ) | ||
|
|
||
| assert result["statusCode"] == 200 | ||
| assert result["body"]["message"] == "Concurrency updated successfully" | ||
| assert result["body"]["function"] == "test-lambda-function" | ||
| assert result["body"]["reservedConcurrency"] == 1000 | ||
|
|
||
|
|
||
| def test_validate_event_success(valid_event): | ||
| target_function, reserved_concurrency = validate_event(valid_event) | ||
|
|
||
| assert target_function == "test-lambda-function" | ||
| assert reserved_concurrency == 10 | ||
|
|
||
|
|
||
| def test_validate_event_missing_target_function(mock_logger): | ||
| event = { | ||
| "reservedConcurrency": 10 | ||
| } | ||
|
|
||
| with pytest.raises(ValueError) as exc_info: | ||
| validate_event(event) | ||
|
|
||
| assert str(exc_info.value) == "targetFunction is required" | ||
| mock_logger.error.assert_called_once_with("Missing required parameter: targetFunction") | ||
|
|
||
|
|
||
| def test_validate_event_missing_reserved_concurrency(mock_logger): | ||
| event = { | ||
| "targetFunction": "test-lambda-function" | ||
| } | ||
|
|
||
| with pytest.raises(ValueError) as exc_info: | ||
| validate_event(event) | ||
|
|
||
| assert str(exc_info.value) == "reservedConcurrency is required" | ||
| mock_logger.error.assert_called_once_with("Missing required parameter: reservedConcurrency") | ||
|
|
||
|
|
||
| def test_validate_event_both_parameters_missing(mock_logger): | ||
| event = {} | ||
|
|
||
| with pytest.raises(ValueError) as exc_info: | ||
| validate_event(event) | ||
|
|
||
| # Should fail on first missing parameter | ||
| assert str(exc_info.value) == "targetFunction is required" | ||
|
|
||
|
|
||
| def test_validate_event_empty_target_function(mock_logger): | ||
| event = { | ||
| "targetFunction": "", | ||
| "reservedConcurrency": 10 | ||
| } | ||
|
|
||
| with pytest.raises(ValueError) as exc_info: | ||
| validate_event(event) | ||
|
|
||
| assert str(exc_info.value) == "targetFunction is required" | ||
| mock_logger.error.assert_called_once_with("Missing required parameter: targetFunction") | ||
|
|
||
|
|
||
| def test_validate_event_reserved_concurrency_zero_is_valid(): | ||
| event = { | ||
| "targetFunction": "test-lambda-function", | ||
| "reservedConcurrency": 0 | ||
| } | ||
|
|
||
| target_function, reserved_concurrency = validate_event(event) | ||
|
|
||
| assert target_function == "test-lambda-function" | ||
| assert reserved_concurrency == 0 | ||
|
|
||
|
|
||
| def test_validate_event_with_additional_fields(): | ||
| event = { | ||
| "targetFunction": "test-lambda-function", | ||
| "reservedConcurrency": 10, | ||
| "extraField": "should-be-ignored" | ||
| } | ||
|
|
||
| target_function, reserved_concurrency = validate_event(event) | ||
|
|
||
| assert target_function == "test-lambda-function" | ||
| assert reserved_concurrency == 10 | ||
|
|
||
|
|
||
| def test_lambda_handler_service_raises_resource_not_found( | ||
| valid_event, context, mock_concurrency_controller_service | ||
| ): | ||
| error_response = { | ||
| 'Error': { | ||
| 'Code': 'ResourceNotFoundException', | ||
| 'Message': 'Function not found' | ||
| } | ||
| } | ||
|
|
||
| mock_concurrency_controller_service.update_function_concurrency.side_effect = ClientError( | ||
| error_response, 'PutFunctionConcurrency' | ||
| ) | ||
|
|
||
| result = lambda_handler(valid_event, context) | ||
|
|
||
| # The decorators convert exceptions to API Gateway error responses | ||
| assert result['statusCode'] == 500 | ||
| body = json.loads(result['body']) | ||
| assert body['message'] == 'Failed to utilise AWS client/resource' | ||
| assert body['err_code'] == 'GWY_5001' | ||
|
|
||
|
|
||
| def test_lambda_handler_service_raises_invalid_parameter( | ||
| context, mock_concurrency_controller_service | ||
| ): | ||
| event = { | ||
| "targetFunction": "test-lambda-function", | ||
| "reservedConcurrency": -1 | ||
| } | ||
|
|
||
| error_response = { | ||
| 'Error': { | ||
| 'Code': 'InvalidParameterValueException', | ||
| 'Message': 'Reserved concurrency value must be non-negative' | ||
| } | ||
| } | ||
|
|
||
| mock_concurrency_controller_service.update_function_concurrency.side_effect = ClientError( | ||
| error_response, 'PutFunctionConcurrency' | ||
| ) | ||
|
|
||
| result = lambda_handler(event, context) | ||
|
|
||
| # The decorators convert exceptions to API Gateway error responses | ||
| assert result['statusCode'] == 500 | ||
| body = json.loads(result['body']) | ||
| assert body['message'] == 'Failed to utilise AWS client/resource' | ||
| assert body['err_code'] == 'GWY_5001' |
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.
Uh oh!
There was an error while loading. Please reload this page.