Skip to content

Commit

Permalink
refactor(BA-620): Add Service Layer to Avoid Direct Volume and Vfolde…
Browse files Browse the repository at this point in the history
…r Operations in Storage-Proxy Handler (#3588)

Co-authored-by: HyeockJinKim <[email protected]>
  • Loading branch information
MintCat98 and HyeockJinKim authored Feb 17, 2025
1 parent 112e36c commit 05727ff
Show file tree
Hide file tree
Showing 23 changed files with 1,284 additions and 625 deletions.
1 change: 1 addition & 0 deletions changes/3588.enhance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add Service Layer to Avoid Direct Volume and Vfolder Operations in Storage-Proxy Handler
1 change: 1 addition & 0 deletions src/ai/backend/common/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ python_distribution(
":src",
"src/ai/backend/common/auth:src",
"src/ai/backend/common/dto/manager:src",
"src/ai/backend/common/dto/storage:src",
"src/ai/backend/common/metrics:src",
"src/ai/backend/common/plugin:src",
"src/ai/backend/common/web/session:src", # not auto-inferred
Expand Down
6 changes: 5 additions & 1 deletion src/ai/backend/common/api_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from aiohttp import web
from aiohttp.web_urldispatcher import UrlMappingMatchInfo
from multidict import CIMultiDictProxy, MultiMapping
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from pydantic_core._pydantic_core import ValidationError

from .exception import (
Expand Down Expand Up @@ -126,6 +126,10 @@ def from_request(cls, request: web.Request) -> Self:
pass


class BaseRequestModel(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)


class BaseResponseModel(BaseModel):
pass

Expand Down
18 changes: 10 additions & 8 deletions src/ai/backend/common/dto/manager/request.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import uuid
from typing import Optional

from pydantic import AliasChoices, BaseModel, Field
from pydantic import AliasChoices, Field

from ai.backend.common import typed_validators as tv
from ai.backend.common.dto.manager.dto import VFolderPermissionDTO
from ai.backend.common.api_handlers import BaseRequestModel
from ai.backend.common.types import VFolderUsageMode

from ...typed_validators import VFolderName
from .field import VFolderPermissionField

class VFolderCreateReq(BaseModel):
name: tv.VFolderName = Field(

class VFolderCreateReq(BaseRequestModel):
name: VFolderName = Field(
description="Name of the vfolder",
)
folder_host: Optional[str] = Field(
validation_alias=AliasChoices("host", "folder_host"),
default=None,
)
usage_mode: VFolderUsageMode = Field(default=VFolderUsageMode.GENERAL)
permission: VFolderPermissionDTO = Field(default=VFolderPermissionDTO.READ_WRITE)
permission: VFolderPermissionField = Field(default=VFolderPermissionField.READ_WRITE)
unmanaged_path: Optional[str] = Field(
validation_alias=AliasChoices("unmanaged_path", "unmanagedPath"),
default=None,
Expand All @@ -31,7 +33,7 @@ class VFolderCreateReq(BaseModel):
)


class RenameVFolderReq(BaseModel):
new_name: tv.VFolderName = Field(
class RenameVFolderReq(BaseRequestModel):
new_name: VFolderName = Field(
description="Name of the vfolder",
)
1 change: 1 addition & 0 deletions src/ai/backend/common/dto/storage/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python_sources(name="src")
21 changes: 21 additions & 0 deletions src/ai/backend/common/dto/storage/field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Optional

from pydantic import BaseModel

from ...types import VolumeID


class VolumeMetaField(BaseModel):
volume_id: VolumeID
backend: str
path: str
fsprefix: Optional[str]
capabilities: list[str]


class VFolderMetaField(BaseModel):
mount_path: str
file_count: int
used_bytes: int
capacity_bytes: int
fs_used_bytes: int
27 changes: 27 additions & 0 deletions src/ai/backend/common/dto/storage/path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from uuid import UUID

from pydantic import Field

from ...api_handlers import BaseRequestModel
from ...types import QuotaScopeType, VolumeID


class VolumeIDPath(BaseRequestModel):
volume_id: VolumeID = Field(
description="A unique identifier for the volume.",
)


class QuotaScopeKeyPath(VolumeIDPath):
scope_type: QuotaScopeType = Field(
description="The type of the quota scope.",
)
scope_uuid: UUID = Field(
description="A unique uuid for the quota scope.",
)


class VFolderKeyPath(QuotaScopeKeyPath):
folder_uuid: UUID = Field(
description="A unique uuid for the virtual folder.",
)
26 changes: 26 additions & 0 deletions src/ai/backend/common/dto/storage/request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Optional

from pydantic import AliasChoices, Field

from ...api_handlers import BaseRequestModel
from ...types import QuotaConfig, VFolderID


class QuotaScopeReq(BaseRequestModel):
options: Optional[QuotaConfig] = Field(
default=None,
description="The options for the quota scope.",
)


class GetVFolderMetaReq(BaseRequestModel):
subpath: str = Field(
description="The subpath of the virtual folder.",
)


class CloneVFolderReq(BaseRequestModel):
dst_vfolder_id: VFolderID = Field(
description="The destination virtual folder ID.",
validation_alias=AliasChoices("dst_vfid", "dst_vfolder_id"),
)
23 changes: 23 additions & 0 deletions src/ai/backend/common/dto/storage/response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import Optional

from pydantic import Field

from ai.backend.common.api_handlers import BaseResponseModel
from ai.backend.common.dto.storage.field import VFolderMetaField, VolumeMetaField


class GetVolumeResponse(BaseResponseModel):
item: VolumeMetaField


class GetVolumesResponse(BaseResponseModel):
items: list[VolumeMetaField]


class QuotaScopeResponse(BaseResponseModel):
used_bytes: Optional[int] = Field(default=0)
limit_bytes: Optional[int] = Field(default=0)


class VFolderMetadataResponse(BaseResponseModel):
item: VFolderMetaField
4 changes: 4 additions & 0 deletions src/ai/backend/common/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import math
import numbers
import textwrap
import uuid
from abc import ABCMeta, abstractmethod
from collections import UserDict, defaultdict, namedtuple
from collections.abc import Iterable
Expand Down Expand Up @@ -974,6 +975,9 @@ def as_trafaret(cls) -> t.Trafaret:
raise NotImplementedError


type VolumeID = uuid.UUID


@attrs.define(slots=True, frozen=True)
class QuotaScopeID:
scope_type: QuotaScopeType
Expand Down
142 changes: 142 additions & 0 deletions src/ai/backend/storage/api/vfolder/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
from typing import Optional, Protocol

from ai.backend.common.api_handlers import APIResponse, BodyParam, PathParam, api_handler
from ai.backend.common.dto.storage.path import QuotaScopeKeyPath, VFolderKeyPath, VolumeIDPath
from ai.backend.common.dto.storage.request import (
CloneVFolderReq,
GetVFolderMetaReq,
QuotaScopeReq,
)
from ai.backend.common.dto.storage.response import (
GetVolumeResponse,
GetVolumesResponse,
VFolderMetadataResponse,
)
from ai.backend.common.types import QuotaConfig, VFolderID, VolumeID

from ...volumes.types import (
QuotaScopeKey,
QuotaScopeMeta,
VFolderKey,
VFolderMeta,
VolumeMeta,
)


class VFolderServiceProtocol(Protocol):
async def get_volume(self, volume_id: VolumeID) -> VolumeMeta: ...

async def get_volumes(self) -> list[VolumeMeta]: ...

async def create_quota_scope(
self, quota_scope_key: QuotaScopeKey, options: Optional[QuotaConfig]
) -> None: ...

async def get_quota_scope(self, quota_scope_key: QuotaScopeKey) -> QuotaScopeMeta: ...

async def update_quota_scope(
self, quota_scope_key: QuotaScopeKey, options: Optional[QuotaConfig]
) -> None: ...

async def delete_quota_scope(self, quota_scope_key: QuotaScopeKey) -> None: ...

async def create_vfolder(self, vfolder_key: VFolderKey) -> None: ...

async def clone_vfolder(self, vfolder_key: VFolderKey, dst_vfolder_id: VFolderID) -> None: ...

async def get_vfolder_info(self, vfolder_key: VFolderKey, subpath: str) -> VFolderMeta: ...

async def delete_vfolder(self, vfolder_key: VFolderKey) -> None: ...


class VFolderHandler:
_storage_service: VFolderServiceProtocol

def __init__(self, storage_service: VFolderServiceProtocol) -> None:
self._storage_service = storage_service

@api_handler
async def get_volume(self, path: PathParam[VolumeIDPath]) -> APIResponse:
volume_meta = await self._storage_service.get_volume(path.parsed.volume_id)
return APIResponse.build(
status_code=200,
response_model=GetVolumeResponse(
item=volume_meta.to_field(),
),
)

@api_handler
async def get_volumes(self) -> APIResponse:
volume_meta_list = await self._storage_service.get_volumes()
return APIResponse.build(
status_code=200,
response_model=GetVolumesResponse(
items=[volume.to_field() for volume in volume_meta_list],
),
)

@api_handler
async def create_quota_scope(
self, path: PathParam[QuotaScopeKeyPath], body: BodyParam[QuotaScopeReq]
) -> APIResponse:
quota_scope_key = QuotaScopeKey.from_quota_scope_path(path.parsed)
await self._storage_service.create_quota_scope(quota_scope_key, body.parsed.options)
return APIResponse.no_content(status_code=204)

@api_handler
async def get_quota_scope(self, path: PathParam[QuotaScopeKeyPath]) -> APIResponse:
quota_scope_key = QuotaScopeKey.from_quota_scope_path(path.parsed)
quota_scope = await self._storage_service.get_quota_scope(quota_scope_key)
return APIResponse.build(
status_code=200,
response_model=quota_scope.to_response(),
)

@api_handler
async def update_quota_scope(
self, path: PathParam[QuotaScopeKeyPath], body: BodyParam[QuotaScopeReq]
) -> APIResponse:
quota_scope_key = QuotaScopeKey.from_quota_scope_path(path.parsed)
await self._storage_service.update_quota_scope(quota_scope_key, body.parsed.options)
return APIResponse.no_content(status_code=204)

@api_handler
async def delete_quota_scope(self, path: PathParam[QuotaScopeKeyPath]) -> APIResponse:
quota_scope_key = QuotaScopeKey.from_quota_scope_path(path.parsed)
await self._storage_service.delete_quota_scope(quota_scope_key)
return APIResponse.no_content(status_code=204)

@api_handler
async def create_vfolder(self, path: PathParam[VFolderKeyPath]) -> APIResponse:
vfolder_key = VFolderKey.from_vfolder_path(path.parsed)
await self._storage_service.create_vfolder(vfolder_key)
return APIResponse.no_content(status_code=204)

@api_handler
async def clone_vfolder(
self, path: PathParam[VFolderKeyPath], body: BodyParam[CloneVFolderReq]
) -> APIResponse:
vfolder_key = VFolderKey.from_vfolder_path(path.parsed)
await self._storage_service.clone_vfolder(vfolder_key, body.parsed.dst_vfolder_id)
return APIResponse.no_content(status_code=204)

@api_handler
async def get_vfolder_info(
self, path: PathParam[VFolderKeyPath], body: BodyParam[GetVFolderMetaReq]
) -> APIResponse:
vfolder_key = VFolderKey.from_vfolder_path(path.parsed)
vfolder_meta = await self._storage_service.get_vfolder_info(
vfolder_key, body.parsed.subpath
)
return APIResponse.build(
status_code=200,
response_model=VFolderMetadataResponse(
item=vfolder_meta.to_field(),
),
)

@api_handler
async def delete_vfolder(self, path: PathParam[VFolderKeyPath]) -> APIResponse:
vfolder_key = VFolderKey.from_vfolder_path(path.parsed)
await self._storage_service.delete_vfolder(vfolder_key)
return APIResponse.no_content(status_code=204)
Loading

0 comments on commit 05727ff

Please sign in to comment.