Skip to content

Commit 4313220

Browse files
committed
Test idle volumes with pipelines
1 parent e5f265b commit 4313220

3 files changed

Lines changed: 170 additions & 5 deletions

File tree

src/dstack/_internal/server/background/scheduled_tasks/idle_volumes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async def process_idle_volumes():
3636
select(VolumeModel.id)
3737
.where(
3838
VolumeModel.status == VolumeStatus.ACTIVE,
39-
VolumeModel.auto_cleanup_enabled != False,
39+
VolumeModel.auto_cleanup_enabled.is_not(False),
4040
VolumeModel.deleted == False,
4141
VolumeModel.lock_expires_at.is_(None),
4242
VolumeModel.id.not_in(lockset),

src/tests/_internal/server/background/scheduled_tasks/test_idle_volumes.py

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,25 @@
2222
get_volume_provisioning_data,
2323
list_events,
2424
)
25+
from dstack._internal.settings import FeatureFlags
2526
from dstack._internal.utils.common import get_current_datetime
2627

2728

29+
@pytest.fixture
30+
def patch_pipeline_processing_flag(monkeypatch: pytest.MonkeyPatch):
31+
def _apply(enabled: bool):
32+
monkeypatch.setattr(FeatureFlags, "PIPELINE_PROCESSING_ENABLED", enabled)
33+
34+
return _apply
35+
36+
2837
@pytest.mark.asyncio
2938
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
30-
class TestProcessIdleVolumes:
39+
class TestProcessIdleVolumesScheduledTask:
40+
@pytest.fixture(autouse=True)
41+
def _patch_feature_flag(self, patch_pipeline_processing_flag):
42+
patch_pipeline_processing_flag(False)
43+
3144
async def test_deletes_idle_volumes(self, test_db, session: AsyncSession):
3245
project = await create_project(session=session)
3346
user = await create_user(session=session)
@@ -71,17 +84,169 @@ async def test_deletes_idle_volumes(self, test_db, session: AsyncSession):
7184
m.return_value = aws_mock
7285
aws_mock.compute.return_value = Mock(spec=ComputeMockSpec)
7386
await process_idle_volumes()
87+
m.assert_called_once()
7488

7589
await session.refresh(volume1)
7690
await session.refresh(volume2)
7791
events = await list_events(session)
92+
assert not volume1.to_be_deleted
7893
assert volume1.deleted
7994
assert volume1.deleted_at is not None
95+
assert not volume2.to_be_deleted
8096
assert not volume2.deleted
8197
assert volume2.deleted_at is None
8298
assert len(events) == 1
8399
assert events[0].message == "Volume deleted due to exceeding auto_cleanup_duration"
84100

101+
async def test_deletes_idle_volume_with_null_auto_cleanup_enabled(
102+
self, test_db, session: AsyncSession
103+
):
104+
project = await create_project(session=session)
105+
user = await create_user(session=session)
106+
volume = await create_volume(
107+
session=session,
108+
project=project,
109+
user=user,
110+
status=VolumeStatus.ACTIVE,
111+
backend=BackendType.AWS,
112+
configuration=get_volume_configuration(
113+
name="test-volume",
114+
auto_cleanup_duration="1h",
115+
),
116+
volume_provisioning_data=get_volume_provisioning_data(),
117+
last_job_processed_at=datetime.datetime.now(datetime.timezone.utc)
118+
- datetime.timedelta(hours=2),
119+
)
120+
volume.auto_cleanup_enabled = None
121+
await session.commit()
122+
123+
with patch(
124+
"dstack._internal.server.services.backends.get_project_backend_by_type_or_error"
125+
) as m:
126+
aws_mock = Mock()
127+
m.return_value = aws_mock
128+
aws_mock.compute.return_value = Mock(spec=ComputeMockSpec)
129+
await process_idle_volumes()
130+
m.assert_called_once()
131+
132+
await session.refresh(volume)
133+
events = await list_events(session)
134+
assert not volume.to_be_deleted
135+
assert volume.deleted
136+
assert volume.deleted_at is not None
137+
assert len(events) == 1
138+
assert events[0].message == "Volume deleted due to exceeding auto_cleanup_duration"
139+
140+
141+
@pytest.mark.asyncio
142+
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
143+
class TestProcessIdleVolumesPipelineTask:
144+
@pytest.fixture(autouse=True)
145+
def _patch_feature_flag(self, patch_pipeline_processing_flag):
146+
patch_pipeline_processing_flag(True)
147+
148+
async def test_deletes_idle_volumes(self, test_db, session: AsyncSession):
149+
project = await create_project(session=session)
150+
user = await create_user(session=session)
151+
152+
config1 = get_volume_configuration(
153+
name="test-volume",
154+
auto_cleanup_duration="1h",
155+
)
156+
config2 = get_volume_configuration(
157+
name="test-volume",
158+
auto_cleanup_duration="3h",
159+
)
160+
volume1 = await create_volume(
161+
session=session,
162+
project=project,
163+
user=user,
164+
status=VolumeStatus.ACTIVE,
165+
backend=BackendType.AWS,
166+
configuration=config1,
167+
volume_provisioning_data=get_volume_provisioning_data(),
168+
last_job_processed_at=datetime.datetime.now(datetime.timezone.utc)
169+
- datetime.timedelta(hours=2),
170+
)
171+
volume2 = await create_volume(
172+
session=session,
173+
project=project,
174+
user=user,
175+
status=VolumeStatus.ACTIVE,
176+
backend=BackendType.AWS,
177+
configuration=config2,
178+
volume_provisioning_data=get_volume_provisioning_data(),
179+
last_job_processed_at=datetime.datetime.now(datetime.timezone.utc)
180+
- datetime.timedelta(hours=2),
181+
)
182+
await session.commit()
183+
184+
with patch(
185+
"dstack._internal.server.services.backends.get_project_backend_by_type_or_error"
186+
) as m:
187+
aws_mock = Mock()
188+
m.return_value = aws_mock
189+
aws_mock.compute.return_value = Mock(spec=ComputeMockSpec)
190+
await process_idle_volumes()
191+
m.assert_not_called()
192+
193+
await session.refresh(volume1)
194+
await session.refresh(volume2)
195+
events = await list_events(session)
196+
assert volume1.to_be_deleted
197+
assert not volume1.deleted
198+
assert volume1.deleted_at is None
199+
assert not volume2.to_be_deleted
200+
assert not volume2.deleted
201+
assert volume2.deleted_at is None
202+
assert len(events) == 1
203+
assert (
204+
events[0].message
205+
== "Volume marked for deletion due to exceeding auto_cleanup_duration"
206+
)
207+
208+
async def test_deletes_idle_volume_with_null_auto_cleanup_enabled(
209+
self, test_db, session: AsyncSession
210+
):
211+
project = await create_project(session=session)
212+
user = await create_user(session=session)
213+
volume = await create_volume(
214+
session=session,
215+
project=project,
216+
user=user,
217+
status=VolumeStatus.ACTIVE,
218+
backend=BackendType.AWS,
219+
configuration=get_volume_configuration(
220+
name="test-volume",
221+
auto_cleanup_duration="1h",
222+
),
223+
volume_provisioning_data=get_volume_provisioning_data(),
224+
last_job_processed_at=datetime.datetime.now(datetime.timezone.utc)
225+
- datetime.timedelta(hours=2),
226+
)
227+
volume.auto_cleanup_enabled = None
228+
await session.commit()
229+
230+
with patch(
231+
"dstack._internal.server.services.backends.get_project_backend_by_type_or_error"
232+
) as m:
233+
aws_mock = Mock()
234+
m.return_value = aws_mock
235+
aws_mock.compute.return_value = Mock(spec=ComputeMockSpec)
236+
await process_idle_volumes()
237+
m.assert_not_called()
238+
239+
await session.refresh(volume)
240+
events = await list_events(session)
241+
assert volume.to_be_deleted
242+
assert not volume.deleted
243+
assert volume.deleted_at is None
244+
assert len(events) == 1
245+
assert (
246+
events[0].message
247+
== "Volume marked for deletion due to exceeding auto_cleanup_duration"
248+
)
249+
85250

86251
@pytest.mark.asyncio
87252
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)

src/tests/_internal/server/background/scheduled_tasks/test_submitted_jobs.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -561,9 +561,9 @@ async def test_fails_job_when_attaching_volume_marked_for_deletion(
561561
job = res.unique().scalar_one()
562562
assert job.status == JobStatus.TERMINATING
563563
assert job.termination_reason == JobTerminationReason.VOLUME_ERROR
564-
assert job.termination_reason_message == "Failed to attach volume"
565-
assert job.instance is not None
566-
assert len(job.instance.volume_attachments) == 0
564+
assert job.termination_reason_message is not None
565+
assert "marked for deletion and cannot be attached" in job.termination_reason_message
566+
assert job.instance is None
567567

568568
@pytest.mark.asyncio
569569
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)

0 commit comments

Comments
 (0)