Skip to content

Commit dcbed32

Browse files
committed
refactor(BA-4092): Refactor VFolder purge API to Action-Service-Repository pattern
- Add PurgeVFolderAction and PurgeVFolderActionResult classes - Add purge() method to VFolderService with status validation - Add purge_vfolder() method to VfolderRepository using execute_purger - Register purge_vfolder processor in VFolderProcessors - Update API handler to use processor instead of direct DB access
1 parent c1be008 commit dcbed32

File tree

6 files changed

+84
-9
lines changed

6 files changed

+84
-9
lines changed

changes/8317.misc.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Refactor VFolder purge API to use Action-Service-Repository pattern.

src/ai/backend/manager/api/vfolder.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
Any,
1717
Concatenate,
1818
ParamSpec,
19-
cast,
2019
)
2120

2221
import aiohttp
@@ -104,6 +103,7 @@
104103
vfolder_status_map,
105104
vfolders,
106105
)
106+
from ai.backend.manager.repositories.base.purger import Purger
107107
from ai.backend.manager.repositories.base.updater import Updater
108108
from ai.backend.manager.repositories.vfolder.updaters import VFolderAttributeUpdaterSpec
109109
from ai.backend.manager.services.vfolder.actions.base import (
@@ -114,6 +114,7 @@
114114
GetVFolderAction,
115115
ListVFolderAction,
116116
MoveToTrashVFolderAction,
117+
PurgeVFolderAction,
117118
RestoreVFolderFromTrashAction,
118119
UpdateVFolderAttributeAction,
119120
)
@@ -1937,14 +1938,14 @@ async def purge(request: web.Request, params: PurgeRequestModel) -> web.Response
19371938
):
19381939
raise InsufficientPrivilege("You are not allowed to purge vfolders")
19391940

1940-
async with root_ctx.db.begin_session() as db_session:
1941-
row = await db_session.scalar(sa.select(VFolderRow).where(VFolderRow.id == folder_id))
1942-
row = cast(VFolderRow | None, row)
1943-
if row is None:
1944-
raise VFolderNotFound(extra_data=folder_id)
1945-
await check_vfolder_status({"status": row.status}, VFolderStatusSet.PURGABLE)
1946-
delete_stmt = sa.delete(VFolderRow).where(VFolderRow.id == folder_id)
1947-
await db_session.execute(delete_stmt)
1941+
try:
1942+
await root_ctx.processors.vfolder.purge_vfolder.wait_for_complete(
1943+
PurgeVFolderAction(
1944+
purger=Purger(row_class=VFolderRow, pk_value=folder_id),
1945+
)
1946+
)
1947+
except VFolderInvalidParameter as e:
1948+
raise InvalidAPIParameters(str(e))
19481949

19491950
return web.Response(status=HTTPStatus.NO_CONTENT)
19501951

src/ai/backend/manager/repositories/vfolder/repository.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
vfolders,
5959
)
6060
from ai.backend.manager.repositories.base.creator import Creator
61+
from ai.backend.manager.repositories.base.purger import Purger, execute_purger
6162
from ai.backend.manager.repositories.base.updater import Updater, execute_updater
6263
from ai.backend.manager.repositories.permission_controller.creators import (
6364
AssociationScopesEntitiesCreatorSpec,
@@ -412,6 +413,19 @@ async def delete_vfolders_forever(self, vfolder_ids: list[uuid.UUID]) -> list[VF
412413

413414
return [self._vfolder_row_to_data(row) for row in vfolder_rows]
414415

416+
417+
@vfolder_repository_resilience.apply()
418+
async def purge_vfolder(self, purger: Purger[VFolderRow]) -> VFolderData:
419+
"""
420+
Permanently delete a VFolder from DB.
421+
This should only be called for VFolders with DELETE_COMPLETE status.
422+
"""
423+
async with self._db.begin_session() as session:
424+
result = await execute_purger(session, purger)
425+
if result is None:
426+
raise VFolderNotFound(extra_data=str(purger.pk_value))
427+
return self._vfolder_row_to_data(result.row)
428+
415429
@vfolder_repository_resilience.apply()
416430
async def get_vfolder_permissions(self, vfolder_id: uuid.UUID) -> list[VFolderPermissionData]:
417431
"""

src/ai/backend/manager/services/vfolder/actions/base.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
VFolderPermission,
2626
VFolderRow,
2727
)
28+
from ai.backend.manager.repositories.base.purger import Purger
2829
from ai.backend.manager.repositories.base.updater import Updater
2930
from ai.backend.manager.services.vfolder.types import (
3031
VFolderBaseInfo,
@@ -382,6 +383,42 @@ def target_entity_id(self) -> str:
382383
return str(self.vfolder_uuid)
383384

384385

386+
@dataclass
387+
class PurgeVFolderAction(VFolderSingleEntityAction):
388+
purger: Purger[VFolderRow]
389+
390+
@override
391+
def entity_id(self) -> Optional[str]:
392+
return str(self.purger.pk_value)
393+
394+
@override
395+
@classmethod
396+
def operation_type(cls) -> str:
397+
return "purge"
398+
399+
@override
400+
@classmethod
401+
def permission_operation_type(cls) -> OperationType:
402+
return OperationType.HARD_DELETE
403+
404+
@override
405+
def target_entity_id(self) -> str:
406+
return str(self.purger.pk_value)
407+
408+
409+
@dataclass
410+
class PurgeVFolderActionResult(VFolderSingleEntityActionResult):
411+
vfolder_uuid: uuid.UUID
412+
413+
@override
414+
def entity_id(self) -> Optional[str]:
415+
return str(self.vfolder_uuid)
416+
417+
@override
418+
def target_entity_id(self) -> str:
419+
return str(self.vfolder_uuid)
420+
421+
385422
@dataclass
386423
class ForceDeleteVFolderAction(VFolderSingleEntityAction):
387424
"""

src/ai/backend/manager/services/vfolder/processors/vfolder.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
ListVFolderActionResult,
2222
MoveToTrashVFolderAction,
2323
MoveToTrashVFolderActionResult,
24+
PurgeVFolderAction,
25+
PurgeVFolderActionResult,
2426
RestoreVFolderFromTrashAction,
2527
RestoreVFolderFromTrashActionResult,
2628
UpdateVFolderAttributeAction,
@@ -45,6 +47,7 @@ class VFolderProcessors(AbstractProcessorPackage):
4547
delete_forever_vfolder: SingleEntityActionProcessor[
4648
DeleteForeverVFolderAction, DeleteForeverVFolderActionResult
4749
]
50+
purge_vfolder: SingleEntityActionProcessor[PurgeVFolderAction, PurgeVFolderActionResult]
4851
force_delete_vfolder: SingleEntityActionProcessor[
4952
ForceDeleteVFolderAction, ForceDeleteVFolderActionResult
5053
]
@@ -67,6 +70,7 @@ def __init__(self, service: VFolderService, action_monitors: list[ActionMonitor]
6770
self.delete_forever_vfolder = SingleEntityActionProcessor(
6871
service.delete_forever, action_monitors
6972
)
73+
self.purge_vfolder = SingleEntityActionProcessor(service.purge, action_monitors)
7074
self.force_delete_vfolder = SingleEntityActionProcessor(
7175
service.force_delete, action_monitors
7276
)
@@ -83,6 +87,7 @@ def supported_actions(self) -> list[ActionSpec]:
8387
MoveToTrashVFolderAction.spec(),
8488
RestoreVFolderFromTrashAction.spec(),
8589
DeleteForeverVFolderAction.spec(),
90+
PurgeVFolderAction.spec(),
8691
ForceDeleteVFolderAction.spec(),
8792
CloneVFolderAction.spec(),
8893
GetTaskLogsAction.spec(),

src/ai/backend/manager/services/vfolder/services/vfolder.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@
7171
ListVFolderActionResult,
7272
MoveToTrashVFolderAction,
7373
MoveToTrashVFolderActionResult,
74+
PurgeVFolderAction,
75+
PurgeVFolderActionResult,
7476
RestoreVFolderFromTrashAction,
7577
RestoreVFolderFromTrashActionResult,
7678
UpdateVFolderAttributeAction,
@@ -532,6 +534,21 @@ async def delete_forever(
532534
await self._remove_vfolder_from_storage(vfolder_data)
533535
return DeleteForeverVFolderActionResult(vfolder_uuid=action.vfolder_uuid)
534536

537+
538+
async def purge(self, action: PurgeVFolderAction) -> PurgeVFolderActionResult:
539+
"""Purge a DELETE_COMPLETE vfolder from DB (admin only)."""
540+
vfolder_uuid = cast(uuid.UUID, action.purger.pk_value)
541+
vfolder_data = await self._vfolder_repository.get_by_id(vfolder_uuid)
542+
if vfolder_data is None:
543+
raise VFolderNotFound(extra_data=str(vfolder_uuid))
544+
if vfolder_data.status not in VFolderStatusSet.PURGABLE:
545+
raise VFolderInvalidParameter(
546+
f"Cannot purge vfolder with status {vfolder_data.status}"
547+
)
548+
549+
await self._vfolder_repository.purge_vfolder(action.purger)
550+
return PurgeVFolderActionResult(vfolder_uuid=vfolder_uuid)
551+
535552
async def force_delete(
536553
self, action: ForceDeleteVFolderAction
537554
) -> ForceDeleteVFolderActionResult:

0 commit comments

Comments
 (0)