Skip to content

Commit f21da89

Browse files
Backend: Handle ServiceNotFoundException in service_manager to sync challenge workers and create services as needed; add unit tests for new behavior. (#4992)
1 parent c7b8c81 commit f21da89

File tree

2 files changed

+76
-0
lines changed

2 files changed

+76
-0
lines changed

apps/challenges/aws_utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,22 @@ def service_manager(
512512
response = update_service_by_challenge_pk(
513513
client, challenge, num_of_tasks, force_new_deployment
514514
)
515+
# Handle ServiceNotFoundException: ECS service was deleted (e.g. after
516+
# AWS key rotation) but DB still has workers set. Sync state and
517+
# either create the service (start/restart) or treat stop as success.
518+
error_code = response.get("Error", {}).get("Code")
519+
if error_code == "ServiceNotFoundException":
520+
if num_of_tasks == 0:
521+
challenge.workers = 0
522+
challenge.save()
523+
return {"ResponseMetadata": {"HTTPStatusCode": HTTPStatus.OK}}
524+
# num_of_tasks > 0: create the service
525+
challenge.workers = None
526+
challenge.save()
527+
client_token = client_token_generator(challenge.pk)
528+
return create_service_by_challenge_pk(
529+
client, challenge, client_token
530+
)
515531
return response
516532
else:
517533
client_token = client_token_generator(challenge.pk)

tests/unit/challenges/test_aws_utils.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,66 @@ def test_service_manager_creates_service(
636636
mock_client, mock_challenge, "mock_client_token"
637637
)
638638

639+
def test_service_manager_service_not_found_stop_returns_success(
640+
self, mock_client, mock_challenge
641+
):
642+
"""When update fails with ServiceNotFoundException and num_of_tasks=0
643+
(stop), treat as success and sync challenge.workers to 0."""
644+
mock_challenge.workers = 1
645+
response_service_not_found = {
646+
"ResponseMetadata": {"HTTPStatusCode": HTTPStatus.BAD_REQUEST},
647+
"Error": {"Code": "ServiceNotFoundException"},
648+
}
649+
650+
with patch(
651+
"challenges.aws_utils.update_service_by_challenge_pk",
652+
return_value=response_service_not_found,
653+
):
654+
response = service_manager(
655+
mock_client, mock_challenge, num_of_tasks=0
656+
)
657+
658+
assert response["ResponseMetadata"]["HTTPStatusCode"] == HTTPStatus.OK
659+
assert mock_challenge.workers == 0
660+
mock_challenge.save.assert_called()
661+
662+
def test_service_manager_service_not_found_start_creates_service(
663+
self, mock_client, mock_challenge
664+
):
665+
"""When update fails with ServiceNotFoundException and num_of_tasks>0
666+
(start), create the service instead."""
667+
mock_challenge.workers = 1
668+
response_service_not_found = {
669+
"ResponseMetadata": {"HTTPStatusCode": HTTPStatus.BAD_REQUEST},
670+
"Error": {"Code": "ServiceNotFoundException"},
671+
}
672+
response_metadata_ok = {
673+
"ResponseMetadata": {"HTTPStatusCode": HTTPStatus.OK}
674+
}
675+
676+
with patch(
677+
"challenges.aws_utils.update_service_by_challenge_pk",
678+
return_value=response_service_not_found,
679+
):
680+
with patch(
681+
"challenges.aws_utils.client_token_generator",
682+
return_value="mock_client_token",
683+
):
684+
with patch(
685+
"challenges.aws_utils.create_service_by_challenge_pk",
686+
return_value=response_metadata_ok,
687+
) as mock_create:
688+
response = service_manager(
689+
mock_client, mock_challenge, num_of_tasks=1
690+
)
691+
692+
assert response == response_metadata_ok
693+
assert mock_challenge.workers is None
694+
mock_challenge.save.assert_called()
695+
mock_create.assert_called_once_with(
696+
mock_client, mock_challenge, "mock_client_token"
697+
)
698+
639699

640700
class TestStopEc2Instance(unittest.TestCase):
641701
@patch("challenges.aws_utils.get_boto3_client")

0 commit comments

Comments
 (0)