Skip to content

Commit 0c2b3f8

Browse files
jopemachineclaude
andcommitted
feat(BA-6155): add metadata-driven chunk-store upload session engine
Introduce the concurrency-safe upload session engine that replaces the single-file-append TUS model. Each chunk is written to its own file under chunks/, named by absolute byte offset; info.json is the source of truth, updated only via atomic rename under a short fcntl.flock window. Heavy chunk payload writes happen lock-free in per-request temp files, so multiple Storage Proxy replicas sharing an NFS mount can upload concurrently without the TOCTOU corruption of the stat-check-then-append model. This commit combines what were previously two stacked PRs — the pure session state model and the on-disk storage class — into a single cohesive unit. They both live in tus_session.py and the model has no consumer on its own, so splitting them across PRs hurt reviewability rather than helping it. Contents: - errors/upload.py: ChunkConflictError (409), UploadSessionCorruptedError (500) - services/upload/tus_session.py: - ChunkRecord / SessionState (committed_offset as the largest contiguous prefix, missing_ranges, progress_percent, find_at_offset) / ChunkAcceptance - TusUploadSession: ensure_initialized, read_state, open_temp_chunk, commit_chunk (idempotent on duplicate (offset,length,sha256), 409 on conflict, no-op on already-completed), assemble, cleanup - stream_chunk_to_temp: bounded-memory streaming + sha256 - cleanup() keeps the completed info.json marker and reclaims only chunk files under the lock, so a late duplicate PATCH that raced completion on another replica observes status=="completed" and no-ops instead of crashing on a vanished temp/target (surfaced by the real-NFS multi-proxy reproduction). - Unit tests for both the model and the on-disk/concurrency behavior. Supersedes the separate state-model change (was BA-6154). Resolves BA-6155. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent e035f55 commit 0c2b3f8

9 files changed

Lines changed: 1080 additions & 0 deletions

File tree

changes/11766.enhance.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Introduce `TusUploadSession`, a concurrency-safe metadata-driven chunk store for resumable (TUS) uploads, as the foundation for corruption-free uploads across storage-proxy replicas that share a filesystem.

src/ai/backend/storage/errors/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@
6262
QuotaScopeNotFoundError,
6363
QuotaTreeNotFoundError,
6464
)
65+
from .upload import (
66+
ChunkConflictError,
67+
UploadSessionCorruptedError,
68+
)
6569
from .vfolder import (
6670
InvalidSubpathError,
6771
VFolderNotFoundError,
@@ -90,6 +94,9 @@
9094
"InvalidDataLengthError",
9195
"ServiceNotInitializedError",
9296
"UploadOffsetMismatchError",
97+
# upload
98+
"ChunkConflictError",
99+
"UploadSessionCorruptedError",
93100
# vfolder
94101
"VFolderNotFoundError",
95102
"InvalidSubpathError",
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
Upload session related exceptions.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from aiohttp import web
8+
9+
from ai.backend.common.exception import (
10+
BackendAIError,
11+
ErrorCode,
12+
ErrorDetail,
13+
ErrorDomain,
14+
ErrorOperation,
15+
)
16+
17+
18+
class ChunkConflictError(BackendAIError, web.HTTPConflict):
19+
"""
20+
Raised when an incoming chunk targets an offset that already holds a
21+
different chunk in the upload session (409 Conflict).
22+
"""
23+
24+
error_type = "https://api.backend.ai/probs/storage/chunk-conflict"
25+
error_title = "Upload Chunk Conflict"
26+
27+
def error_code(self) -> ErrorCode:
28+
return ErrorCode(
29+
domain=ErrorDomain.STORAGE_PROXY,
30+
operation=ErrorOperation.UPDATE,
31+
error_detail=ErrorDetail.CONFLICT,
32+
)
33+
34+
35+
class UploadSessionCorruptedError(BackendAIError, web.HTTPInternalServerError):
36+
"""
37+
Raised when the on-disk upload session metadata cannot be parsed or is
38+
structurally invalid.
39+
"""
40+
41+
error_type = "https://api.backend.ai/probs/storage/upload-session-corrupted"
42+
error_title = "Upload Session Corrupted"
43+
44+
def error_code(self) -> ErrorCode:
45+
return ErrorCode(
46+
domain=ErrorDomain.STORAGE_PROXY,
47+
operation=ErrorOperation.READ,
48+
error_detail=ErrorDetail.INTERNAL_ERROR,
49+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Chunk-based TUS upload session services.
3+
"""

0 commit comments

Comments
 (0)