| Author | Gyubong Lee (gbl@lablup.com) |
|---|---|
| Status | Draft |
| Created | 2026-01-22 |
| Created-Version | 26.3.0 |
| Target-Version | |
| Implemented-Version |
Currently, artifact storage has no usage tracking or capacity limits. When artifacts are imported, they are stored without any visibility into how much space is being consumed or any mechanism to prevent storage exhaustion.
This creates operational risks:
- Storage can be exhausted without warning
- No visibility into storage utilization
- No way to enforce capacity planning or cost control
The artifact import flow supports two storage destinations, and both need quota enforcement:
- Default (StorageNamespace): No quota system exists
- VFolder destination: Quota system exists (
max_quota_scope_size) but not integrated into artifact import pre-check
StorageNamespaceRow: Tracks basic namespace information (id,storage_id,namespace). No quota-related fields.ArtifactRevisionRow: Already trackssizefor individual revisions.AssociationArtifactsStorageRow: Links artifact revisions to storage namespaces.
The artifact import flow (import_revision()) supports two destinations:
| Destination | When | Current Quota |
|---|---|---|
| StorageNamespace | vfolder_id is None |
None |
| VFolder | vfolder_id is provided |
max_quota_scope_size in resource policy (enforced by storage proxy at write time) |
VFolders have an existing quota system:
VFolderRow.quota_scope_id→ Links to user or project- Resource policies define
max_quota_scope_size - Storage proxy enforces quota at filesystem write time
Problem: The artifact import does not pre-check VFolder quota before starting the import. Large imports can fail mid-way when the storage proxy rejects writes due to quota limits.
Create a unified quota enforcement layer that performs pre-validation before artifact import begins, regardless of storage destination.
Create ArtifactStorageQuotaService that handles both storage destinations with a single entry point:
class ArtifactStorageQuotaService:
"""Unified quota service for artifact storage."""
async def check_quota(
self,
storage_destination: StorageDestination,
additional_size: int,
) -> None:
match storage_destination:
case StorageNamespaceDestination(namespace_id):
await self._check_storage_namespace_quota(namespace_id, additional_size)
case VFolderDestination(vfolder_id, quota_scope_id):
await self._check_vfolder_quota(vfolder_id, quota_scope_id, additional_size)Each storage destination has different quota mechanisms:
| Destination | Quota Source | Usage Source | Details |
|---|---|---|---|
| StorageNamespace | StorageNamespaceRow.max_size (NEW) |
Aggregated from artifact_revisions via association table |
storage_namespace.md |
| VFolder | max_quota_scope_size from resource policy |
Storage proxy API | vfolder_storage.md |
┌─────────────────────────────────────────────────────────────────────────────┐
│ import_revision(action) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Get revision_data (includes size) │
│ │
│ 2. Determine storage destination │
│ ┌─────────────────────────────┬────────────────────────────────────┐ │
│ │ vfolder_id provided? │ │ │
│ └──────────┬──────────────────┴────────────────────┬───────────────┘ │
│ │ YES │ NO │
│ ▼ ▼ │
│ ┌─────────────────────────┐ ┌─────────────────────────────┐ │
│ │ VFolderDestination │ │ StorageNamespaceDestination│ │
│ │ - vfolder_id │ │ - namespace_id │ │
│ │ - quota_scope_id │ │ │ │
│ └───────────┬─────────────┘ └───────────────┬─────────────┘ │
│ │ │ │
│ └──────────────┬──────────────────────────┘ │
│ ▼ │
│ 3. ┌──────────────────────────────────────────────────────────────────┐ │
│ │ ArtifactStorageQuotaService.check_quota() │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────┴──────────────┐ │
│ ▼ ▼ │
│ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ _check_vfolder_quota() │ │ _check_storage_namespace_quota()│ │
│ └───────────┬─────────────┘ └───────────────┬─────────────────┘ │
│ │ │ │
│ └──────────────┬────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ Quota Exceeded? │ │
│ └──────────────┬───────────────┘ │
│ YES │ │ NO │
│ ▼ ▼ │
│ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ Raise appropriate error │ │ 4. Proceed with import │ │
│ │ - VFolderQuotaExceeded │ │ - Call storage proxy │ │
│ │ - StorageNamespace │ │ - Update status │ │
│ │ QuotaExceeded │ │ - Associate with storage │ │
│ └─────────────────────────┘ └─────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
StorageNamespace: Usage is aggregated from artifact revisions linked via association table.
┌─────────────────────┐ ┌─────────────────────────────────┐
│ storage_namespace │ │ association_artifacts_storages │
├─────────────────────┤ ├─────────────────────────────────┤
│ id │◄────│ storage_namespace_id │
│ max_size (NEW) │ │ artifact_revision_id ───────────┼──┐
└─────────────────────┘ └─────────────────────────────────┘ │
│
┌─────────────────────────────────┐ │
│ artifact_revisions │ │
├─────────────────────────────────┤ │
│ id ◄────────────────────────────┼──┘
│ size │
└─────────────────────────────────┘
VFolder: Usage is queried from storage proxy, limit comes from resource policies.
┌─────────────────────┐ ┌─────────────────────────────────┐
│ vfolders │ │ user_resource_policies / │
├─────────────────────┤ │ project_resource_policies │
│ id │ ├─────────────────────────────────┤
│ quota_scope_id ─────┼────►│ max_quota_scope_size │
└─────────────────────┘ └─────────────────────────────────┘
│
│ VFolderID
▼
┌─────────────────────┐
│ Storage Proxy │
├─────────────────────┤
│ get_quota_scope_ │
│ usage(quota_scope) │──► Current usage in bytes
└─────────────────────┘
| Aspect | StorageNamespace | VFolder |
|---|---|---|
| Limit Source | storage_namespace.max_size |
resource_policy.max_quota_scope_size |
| Usage Source | DB aggregation via association table | Storage proxy API |
| Unlimited Value | NULL |
-1 |
| Scope | Per namespace | Per quota scope (user/project) |
class StorageQuotaExceededError(BackendError):
"""Base class for quota exceeded errors."""
pass
class StorageNamespaceQuotaExceededError(StorageQuotaExceededError):
"""Raised when StorageNamespace quota would be exceeded."""
namespace_id: uuid.UUID
current_size: int
max_size: int
requested_size: int
class VFolderQuotaExceededError(StorageQuotaExceededError):
"""Raised when VFolder quota scope limit would be exceeded."""
vfolder_id: VFolderID
quota_scope_id: QuotaScopeID
current_size: int
max_size: int
requested_size: intReturns storage namespace usage statistics.
{
"namespace_id": "...",
"total_size": 10737418240,
"max_size": 107374182400,
"revision_count": 42,
"utilization_percent": 10.0
}Updates quota for a storage namespace. Admin-only.
{ "max_size": 107374182400 } // or null for unlimited- Quota check passes when under limit
- Quota check passes when
max_sizeis NULL (unlimited) - Quota check raises error when limit would be exceeded
- Import flow rejects artifact when quota exceeded
- Pre-check queries storage proxy for current usage
- Quota check uses
max_quota_scope_sizefrom resource policy - Import is rejected before starting if quota would be exceeded
- Handles case when resource policy limit is unlimited (-1)
- Correct quota system is selected based on
vfolder_idpresence - Error messages clearly indicate which quota was exceeded
When storage usage approaches the configured limit, the system could notify administrators:
- Threshold levels: 80%, 90%, 95% utilization
- Integration with existing notification system