feat: 사용자별 컴퓨트·스토리지 리소스 한도 및 이중 삭제 보호#65
Open
qixiangme wants to merge 6 commits into
Open
Conversation
[Storage] - Add UserStorageLimits.StorageGB: rejects uploads when total GB across all user containers + new file exceeds limit (checked at upload time via ListObjects sum, not just container count) - Add CountByOwner to repo: single COUNT query instead of loading all rows - Add RCP_MAX_CONTAINERS_PER_USER and RCP_MAX_STORAGE_GB_PER_USER env vars - Flip delete order to DB-first → Swift-last: 0 rows affected = already deleted by concurrent request → skip Swift call, return ErrContainerNotFound - Make Swift DeleteContainer / DeleteObject 404-tolerant (already gone = success) [Compute] - Make OpenStack DeleteServer 404-tolerant (VM already terminated = success) - DeleteByOpenstackID now returns (bool, error): 0 rows = concurrent delete already handled → return ErrInstanceNotFound, preventing quota double-release Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#1 - Disk exhaustion before limit check: Apply MaxBytesReader(StorageLimitBytes) before FormFile so Go never buffers more than the user quota to disk. Returns 413 if exceeded. #2 - Swift container orphaned on DB-first delete failure: Revert delete order to Swift-first (DB-second). With 404-tolerance already in place, concurrent double-deletes are handled: second request gets Swift 404 -> success, then DB 0 rows -> ErrContainerNotFound. Also make ListObjects 404-tolerant (return empty) so concurrent requests don't fail at the ListObjects step. #3 - Off-by-one: > should be >=: totalBytes+additionalBytes >= limitBytes now correctly rejects uploads that would bring usage to exactly the limit, not only over it. #6 - N sequential Swift ListObjects on every upload: Parallelize per-container ListObjects calls with goroutines + WaitGroup. Reduces latency from O(N*roundtrip) to O(max(roundtrip)). #10 - Wrong error precedence (409 before 429): Move ensureUserStorageLimits before FindByName in CreateContainer so a user at their limit gets 429 instead of 409 when the name also exists. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…esource-quota # Conflicts: # internal/domain/storage/handler.go # internal/server/app.go
jisung-02
reviewed
Jun 6, 2026
| ) | ||
|
|
||
| if err := h.Svc.UploadObject(c.Request.Context(), id, containerName, objectName, fileStream, contentType); err != nil { | ||
| size := c.Request.ContentLength |
Contributor
There was a problem hiding this comment.
파일 크기를 ContentLength로 산정하는 부분에서 Content-Length 헤더는 클라이언트가 보내는 값이라 신뢰할 수 없고, 누락되면 -1이 됩니다. 실제 쿼터 산정은 스트림으로 읽은 바이트 수 기준으로 검증하는 게 안전합니다.
| // 한도를 초과하는 파일은 디스크를 채우기 전에 차단된다. | ||
| if maxBytes := h.Svc.StorageLimitBytes(); maxBytes > 0 { | ||
| c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxBytes) | ||
| } |
Contributor
There was a problem hiding this comment.
남은 쿼터 전체를 한 번의 업로드 상한으로 걸면, 이미 사용 중인 용량이 있어도 한도까지 통과될 수 있습니다. (남은 용량 = 한도 − 현재 사용량)으로 상한을 잡는 편이 의도에 맞습니다.
| return nil | ||
| } | ||
|
|
||
| func (s *Service) ensureUserStorageSizeLimit(ctx context.Context, ownerID uuid.UUID, additionalBytes int64) error { |
Contributor
There was a problem hiding this comment.
업로드마다 전체 객체를 스캔하면 객체 수가 늘어날수록 비용이 커집니다. 또한 검사 시점과 실제 쓰기 시점 사이에 동시 업로드가 끼어들면 한도를 초과할 수 있는 TOCTOU 여지가 있습니다. 사용량을 별도로 집계/캐싱하거나 원자적 검증을 고려해볼 만합니다.
| } | ||
|
|
||
| limitBytes := int64(s.userLimits.StorageGB) * 1024 * 1024 * 1024 | ||
| if totalBytes+additionalBytes >= limitBytes { |
Contributor
There was a problem hiding this comment.
정확히 한도와 같은 크기에서 허용/거부 의도가 무엇인지 확인 부탁드립니다. >=이면 한도 정확히 채우는 것도 거부됩니다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
개요
사용자별 컴퓨트(VM)·스토리지 리소스 상한을 도입하고,
이중 삭제(double-delete) 경쟁 조건에서 할당량이 중복 반납되는 문제를 수정한다.
변경
컴퓨트 — 사용자별 사용량 한도
UserUsageLimits구조체: 인스턴스 수·vCPU·RAM·Disk 누적 스펙 합산 기반 상한ensureUserUsageLimits— flavor 스펙 합산 후 한도 초과 시 거부DeleteServer404-tolerant: 이미 삭제된 VM은 성공으로 처리DeleteByOpenstackID→(bool, error): 0 rows = 동시 삭제 감지 →ErrInstanceNotFound(할당량 이중 반납 방지)스토리지 — 사용자별 한도 + 이중 삭제 방지
CountByOwner(단일 COUNT 쿼리)sync.WaitGroup병렬ListObjects— O(N·RTT) → O(RTT)MaxBytesReader를FormFile전에 적용, 초과 시 413 반환DeleteContainer/DeleteObject/ListObjects404-tolerantDelete→(bool, error): 0 rows = 동시 요청 감지 → 할당량 이중 반납 없음환경변수 및 현재 적용 상한값
RCP_MAX_INSTANCES_PER_USERRCP_MAX_VCPUS_PER_USERRCP_MAX_RAM_MB_PER_USERRCP_MAX_DISK_GB_PER_USERRCP_MAX_CONTAINERS_PER_USERRCP_MAX_STORAGE_GB_PER_USER테스트
compute/service_test.go: 인스턴스 수·리소스 한도 거부, 이중 삭제 →ErrInstanceNotFoundstorage/service_test.go: 버킷 개수·GB 한도 거부, 경계값(>=), 이중 삭제 →ErrContainerNotFound, Swift-first 순서 검증, 무제한 시 DB 조회 스킵후속 작업