From 97ad8fab88ccc4e1a546c4fdb9d6a6c62f2abb3c Mon Sep 17 00:00:00 2001 From: Jvst Me Date: Wed, 22 Apr 2026 01:19:17 +0200 Subject: [PATCH] Fix deleting idle instance from fleet with runs --- .../_internal/server/services/fleets.py | 4 +- .../_internal/server/routers/test_fleets.py | 59 ++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/dstack/_internal/server/services/fleets.py b/src/dstack/_internal/server/services/fleets.py index 13f383fc0..3129bcbb0 100644 --- a/src/dstack/_internal/server/services/fleets.py +++ b/src/dstack/_internal/server/services/fleets.py @@ -919,7 +919,9 @@ def is_fleet_in_use(fleet_model: FleetModel, instance_nums: Optional[List[int]] if instance_nums is not None: selected_instance_in_use = [i for i in instances_in_use if i.instance_num in instance_nums] active_runs = [r for r in fleet_model.runs if not r.status.is_finished()] - return len(selected_instance_in_use) > 0 or len(instances_in_use) == 0 and len(active_runs) > 0 + return len(selected_instance_in_use) > 0 or ( + instance_nums is None and len(instances_in_use) == 0 and len(active_runs) > 0 + ) def is_fleet_empty(fleet_model: FleetModel) -> bool: diff --git a/src/tests/_internal/server/routers/test_fleets.py b/src/tests/_internal/server/routers/test_fleets.py index 44971db9a..196f263a6 100644 --- a/src/tests/_internal/server/routers/test_fleets.py +++ b/src/tests/_internal/server/routers/test_fleets.py @@ -1633,7 +1633,38 @@ async def test_terminates_fleet_instances( @pytest.mark.asyncio @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) - async def test_returns_400_when_fleets_in_use( + async def test_returns_400_when_fleet_in_use( + self, test_db, session: AsyncSession, client: AsyncClient + ): + user = await create_user(session, global_role=GlobalRole.USER) + project = await create_project(session) + await add_project_member( + session=session, project=project, user=user, project_role=ProjectRole.USER + ) + fleet = await create_fleet(session=session, project=project) + repo = await create_repo( + session=session, + project_id=project.id, + ) + await create_run( + session=session, + project=project, + repo=repo, + user=user, + fleet=fleet, + ) + response = await client.post( + f"/api/project/{project.name}/fleets/delete", + headers=get_auth_headers(user.token), + json={"names": [fleet.name]}, + ) + assert response.status_code == 400 + await session.refresh(fleet) + assert not fleet.deleted + + @pytest.mark.asyncio + @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True) + async def test_returns_400_when_fleet_instance_in_use( self, test_db, session: AsyncSession, client: AsyncClient ): user = await create_user(session, global_role=GlobalRole.USER) @@ -1800,10 +1831,34 @@ async def test_terminates_fleet_instances( session=session, project=project, instance_num=2, + status=InstanceStatus.IDLE, + ) + instance3 = await create_instance( + session=session, + project=project, + instance_num=3, + status=InstanceStatus.BUSY, ) fleet.instances.append(instance1) fleet.instances.append(instance2) - await session.commit() + fleet.instances.append(instance3) + repo = await create_repo( + session=session, + project_id=project.id, + ) + # Run assigned to instance 3. Should not interfere with deleting instance 1. + run = await create_run( + session=session, + project=project, + repo=repo, + user=user, + fleet=fleet, + ) + await create_job( + session=session, + run=run, + instance=instance3, + ) response = await client.post( f"/api/project/{project.name}/fleets/delete_instances", headers=get_auth_headers(user.token),