Skip to content

Commit eee4e8b

Browse files
authored
Merge pull request #89 from Serverless-Devs/copilot/fix-terminated-sandbox-issue
fix: raise ResourceNotExistError in delete_sandbox when data plane returns "sandbox not found"
2 parents e0688e7 + eec1443 commit eee4e8b

2 files changed

Lines changed: 151 additions & 4 deletions

File tree

agentrun/sandbox/client.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,11 @@ async def delete_sandbox_async(
717717
Sandbox: 停止后的 Sandbox 对象
718718
719719
Raises:
720-
ResourceNotExistError: Sandbox 不存在
720+
ResourceNotExistError: Sandbox 不存在(包括 HTTP 404 与数据面业务层
721+
not-found 两种情形)。调用方可 catch 此异常实现幂等删除。
722+
Sandbox does not exist (covers both HTTP 404 and data-plane
723+
business-level not-found). Callers can catch this exception
724+
for idempotent delete logic.
721725
ClientError: 客户端错误
722726
ServerError: 服务器错误
723727
"""
@@ -728,11 +732,24 @@ async def delete_sandbox_async(
728732

729733
# 判断返回结果是否成功
730734
if result.get("code") != "SUCCESS":
735+
message = result.get("message", "")
736+
# 数据面报告 sandbox 不存在时,与 HTTP 404 路径保持一致,
737+
# 统一抛出 ResourceNotExistError,方便调用方幂等处理。
738+
# When the data plane reports sandbox not found, raise
739+
# ResourceNotExistError for consistency with the HTTP 404 path.
740+
# Callers can catch ResourceNotExistError to implement idempotent
741+
# deletion (e.g. when TERMINATED instances still appear in list
742+
# results but have already been removed from the data plane).
743+
# Note: long-term the server should return a stable error_code
744+
# (e.g. SandboxNotFound) so the SDK can match on that instead
745+
# of a message string.
746+
if "sandbox not found" in message.lower():
747+
raise ResourceNotExistError("Sandbox", sandbox_id)
731748
raise ClientError(
732749
status_code=0,
733750
message=(
734751
"Failed to stop sandbox:"
735-
f" {result.get('message', 'Unknown error')}"
752+
f" {message or 'Unknown error'}"
736753
),
737754
)
738755

@@ -757,7 +774,11 @@ def delete_sandbox(
757774
Sandbox: 停止后的 Sandbox 对象
758775
759776
Raises:
760-
ResourceNotExistError: Sandbox 不存在
777+
ResourceNotExistError: Sandbox 不存在(包括 HTTP 404 与数据面业务层
778+
not-found 两种情形)。调用方可 catch 此异常实现幂等删除。
779+
Sandbox does not exist (covers both HTTP 404 and data-plane
780+
business-level not-found). Callers can catch this exception
781+
for idempotent delete logic.
761782
ClientError: 客户端错误
762783
ServerError: 服务器错误
763784
"""
@@ -768,11 +789,24 @@ def delete_sandbox(
768789

769790
# 判断返回结果是否成功
770791
if result.get("code") != "SUCCESS":
792+
message = result.get("message", "")
793+
# 数据面报告 sandbox 不存在时,与 HTTP 404 路径保持一致,
794+
# 统一抛出 ResourceNotExistError,方便调用方幂等处理。
795+
# When the data plane reports sandbox not found, raise
796+
# ResourceNotExistError for consistency with the HTTP 404 path.
797+
# Callers can catch ResourceNotExistError to implement idempotent
798+
# deletion (e.g. when TERMINATED instances still appear in list
799+
# results but have already been removed from the data plane).
800+
# Note: long-term the server should return a stable error_code
801+
# (e.g. SandboxNotFound) so the SDK can match on that instead
802+
# of a message string.
803+
if "sandbox not found" in message.lower():
804+
raise ResourceNotExistError("Sandbox", sandbox_id)
771805
raise ClientError(
772806
status_code=0,
773807
message=(
774808
"Failed to stop sandbox:"
775-
f" {result.get('message', 'Unknown error')}"
809+
f" {message or 'Unknown error'}"
776810
),
777811
)
778812

tests/unittests/sandbox/test_client.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,119 @@ def test_delete_sandbox_not_exist(
810810
with pytest.raises(ResourceNotExistError):
811811
client.delete_sandbox("nonexistent")
812812

813+
@patch("agentrun.sandbox.client.SandboxControlAPI")
814+
@patch("agentrun.sandbox.client.SandboxDataAPI")
815+
def test_delete_sandbox_not_found_in_response_raises_resource_not_exist(
816+
self, mock_data_api_class, mock_control_api_class
817+
):
818+
"""数据面业务层返回 not-found 时,与 HTTP 404 路径统一抛 ResourceNotExistError。
819+
820+
Callers can catch ResourceNotExistError for idempotent deletion when the
821+
control plane still lists a TERMINATED sandbox but the data plane has
822+
already removed it (e.g. ``except ResourceNotExistError: pass``).
823+
"""
824+
mock_data_api = MagicMock()
825+
mock_data_api.delete_sandbox.return_value = {
826+
"code": "FAILED",
827+
"message": "sandbox not found",
828+
}
829+
mock_data_api_class.return_value = mock_data_api
830+
831+
client = SandboxClient()
832+
with pytest.raises(ResourceNotExistError):
833+
client.delete_sandbox("sandbox-123")
834+
835+
@patch("agentrun.sandbox.client.SandboxControlAPI")
836+
@patch("agentrun.sandbox.client.SandboxDataAPI")
837+
def test_delete_sandbox_not_found_case_insensitive(
838+
self, mock_data_api_class, mock_control_api_class
839+
):
840+
"""大小写变体(如 'Sandbox NOT FOUND')也应触发 ResourceNotExistError。"""
841+
mock_data_api = MagicMock()
842+
mock_data_api.delete_sandbox.return_value = {
843+
"code": "FAILED",
844+
"message": "Sandbox NOT FOUND",
845+
}
846+
mock_data_api_class.return_value = mock_data_api
847+
848+
client = SandboxClient()
849+
with pytest.raises(ResourceNotExistError):
850+
client.delete_sandbox("sandbox-123")
851+
852+
@patch("agentrun.sandbox.client.SandboxControlAPI")
853+
@patch("agentrun.sandbox.client.SandboxDataAPI")
854+
def test_delete_sandbox_other_failure_message_raises_client_error(
855+
self, mock_data_api_class, mock_control_api_class
856+
):
857+
"""无关 not-found 的失败消息(如 'sandbox is busy')应仍抛 ClientError。"""
858+
mock_data_api = MagicMock()
859+
mock_data_api.delete_sandbox.return_value = {
860+
"code": "FAILED",
861+
"message": "sandbox is busy",
862+
}
863+
mock_data_api_class.return_value = mock_data_api
864+
865+
client = SandboxClient()
866+
with pytest.raises(ClientError, match="Failed to stop sandbox"):
867+
client.delete_sandbox("sandbox-123")
868+
869+
@patch("agentrun.sandbox.client.SandboxControlAPI")
870+
@patch("agentrun.sandbox.client.SandboxDataAPI")
871+
def test_delete_sandbox_empty_message_raises_client_error(
872+
self, mock_data_api_class, mock_control_api_class
873+
):
874+
"""message 为空时不应误触 not-found 逻辑,应抛 ClientError。"""
875+
mock_data_api = MagicMock()
876+
mock_data_api.delete_sandbox.return_value = {
877+
"code": "FAILED",
878+
"message": "",
879+
}
880+
mock_data_api_class.return_value = mock_data_api
881+
882+
client = SandboxClient()
883+
with pytest.raises(ClientError, match="Failed to stop sandbox"):
884+
client.delete_sandbox("sandbox-123")
885+
886+
@patch("agentrun.sandbox.client.SandboxControlAPI")
887+
@patch("agentrun.sandbox.client.SandboxDataAPI")
888+
@pytest.mark.asyncio
889+
async def test_delete_sandbox_async_not_found_in_response_raises_resource_not_exist(
890+
self, mock_data_api_class, mock_control_api_class
891+
):
892+
"""数据面业务层返回 not-found 时(async),与 HTTP 404 路径统一抛 ResourceNotExistError。"""
893+
mock_data_api = MagicMock()
894+
mock_data_api.delete_sandbox_async = AsyncMock(
895+
return_value={
896+
"code": "FAILED",
897+
"message": "sandbox not found",
898+
}
899+
)
900+
mock_data_api_class.return_value = mock_data_api
901+
902+
client = SandboxClient()
903+
with pytest.raises(ResourceNotExistError):
904+
await client.delete_sandbox_async("sandbox-123")
905+
906+
@patch("agentrun.sandbox.client.SandboxControlAPI")
907+
@patch("agentrun.sandbox.client.SandboxDataAPI")
908+
@pytest.mark.asyncio
909+
async def test_delete_sandbox_async_other_failure_raises_client_error(
910+
self, mock_data_api_class, mock_control_api_class
911+
):
912+
"""无关 not-found 的失败消息(async)应仍抛 ClientError。"""
913+
mock_data_api = MagicMock()
914+
mock_data_api.delete_sandbox_async = AsyncMock(
915+
return_value={
916+
"code": "FAILED",
917+
"message": "sandbox is busy",
918+
}
919+
)
920+
mock_data_api_class.return_value = mock_data_api
921+
922+
client = SandboxClient()
923+
with pytest.raises(ClientError, match="Failed to stop sandbox"):
924+
await client.delete_sandbox_async("sandbox-123")
925+
813926
@patch("agentrun.sandbox.client.SandboxControlAPI")
814927
@patch("agentrun.sandbox.client.SandboxDataAPI")
815928
@pytest.mark.asyncio

0 commit comments

Comments
 (0)