Skip to content

Commit fccee9f

Browse files
jopemachineclaude
andcommitted
refactor(BA-6156): type the upload session id as TusSessionId
UploadTokenData.session is now typed as the common TusSessionId NewType so the session id flows type-safely from the token through the handlers into the Valkey-backed engine. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c6f215e commit fccee9f

4 files changed

Lines changed: 118 additions & 40 deletions

File tree

src/ai/backend/storage/api/client.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,20 @@
4242
from ai.backend.common.metrics.http import build_api_metric_middleware
4343
from ai.backend.common.middlewares.exception import general_exception_middleware
4444
from ai.backend.common.typed_validators import PydanticJWTValidator
45-
from ai.backend.common.types import BinarySize, VFolderID
45+
from ai.backend.common.types import BinarySize, TusSessionId, VFolderID
4646
from ai.backend.logging import BraceStyleAdapter
4747
from ai.backend.storage import __version__
4848
from ai.backend.storage.dto.context import StorageRootCtx
49-
from ai.backend.storage.errors import InvalidAPIParameters, UploadOffsetMismatchError
49+
from ai.backend.storage.errors import (
50+
InvalidAPIParameters,
51+
UploadChunkExceedsTotalSizeError,
52+
UploadOffsetMismatchError,
53+
)
5054
from ai.backend.storage.services.file_stream.zip import (
5155
ZipArchiveStreamReader,
5256
)
53-
from ai.backend.storage.services.upload.tus_session import (
54-
TusUploadSession,
55-
TusUploadSessionArgs,
56-
)
57+
from ai.backend.storage.services.upload.tus_session import TusUploadSession
58+
from ai.backend.storage.services.upload.types import TusUploadSessionArgs
5759
from ai.backend.storage.types import SENTINEL, TusChunkUploadStreamReader
5860
from ai.backend.storage.utils import (
5961
CheckParamSource,
@@ -99,7 +101,7 @@ class UploadTokenData(TypedDict):
99101
volume: str
100102
vfid: VFolderID
101103
relpath: str
102-
session: str
104+
session: TusSessionId
103105
size: int
104106

105107

@@ -349,10 +351,11 @@ class Params(TypedDict):
349351
session_id=token_data["session"],
350352
total_size=int(token_data["size"]),
351353
valkey_client=ctx.valkey_tus_client,
354+
lock_factory=ctx.tus_lock_factory,
352355
)
353356
)
354357
state = await session.read_state()
355-
headers = _tus_response_headers(
358+
headers = _prepare_tus_session_headers(
356359
upload_offset=state.committed_offset,
357360
upload_length=int(token_data["size"]),
358361
)
@@ -428,31 +431,30 @@ class Params(TypedDict):
428431
session_id=token_data["session"],
429432
total_size=total_size,
430433
valkey_client=ctx.valkey_tus_client,
434+
lock_factory=ctx.tus_lock_factory,
431435
)
432436
)
433437
await session.ensure_initialized()
434438

435439
upload_stream = TusChunkUploadStreamReader(
436440
request.content, request.content_type, DEFAULT_CHUNK_SIZE
437441
)
438-
temp_chunk, length, sha256 = await session.write_temp_chunk(
439-
client_offset, upload_stream
440-
)
442+
written = await session.write_temp_chunk(client_offset, upload_stream)
441443
try:
442-
if client_offset + length > total_size:
443-
raise UploadOffsetMismatchError(
444-
f"Chunk at offset {client_offset} with length {length} "
444+
if client_offset + written.length > total_size:
445+
raise UploadChunkExceedsTotalSizeError(
446+
f"Chunk at offset {client_offset} with length {written.length} "
445447
f"exceeds declared size {total_size}"
446448
)
447449
acceptance = await session.commit_chunk(
448450
offset=client_offset,
449-
chunk_path=temp_chunk.path,
450-
length=length,
451-
sha256=sha256,
451+
chunk_path=written.path,
452+
length=written.length,
453+
sha256=written.sha256,
452454
)
453455
except BaseException:
454-
if temp_chunk.path.exists():
455-
await asyncio.to_thread(temp_chunk.path.unlink)
456+
if written.path.exists():
457+
await asyncio.to_thread(written.path.unlink)
456458
raise
457459

458460
state = acceptance.state
@@ -464,7 +466,7 @@ class Params(TypedDict):
464466
await session.assemble(target_path)
465467
await session.cleanup()
466468

467-
headers = _tus_response_headers(
469+
headers = _prepare_tus_session_headers(
468470
upload_offset=state.committed_offset,
469471
upload_length=total_size,
470472
)
@@ -478,7 +480,7 @@ def _resolve_tus_upload_session_dir(volume: AbstractVolume, token_data: UploadTo
478480
_TUS_HEADER_LIST = "Tus-Resumable, Upload-Length, Upload-Metadata, Upload-Offset, Content-Type"
479481

480482

481-
def _tus_response_headers(*, upload_offset: int, upload_length: int) -> dict[str, str]:
483+
def _prepare_tus_session_headers(*, upload_offset: int, upload_length: int) -> dict[str, str]:
482484
return {
483485
"Access-Control-Allow-Origin": "*",
484486
"Access-Control-Allow-Headers": _TUS_HEADER_LIST,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
)
6565
from .upload import (
6666
ChunkConflictError,
67+
UploadChunkExceedsTotalSizeError,
6768
UploadSessionCorruptedError,
6869
)
6970
from .vfolder import (
@@ -96,6 +97,7 @@
9697
"UploadOffsetMismatchError",
9798
# upload
9899
"ChunkConflictError",
100+
"UploadChunkExceedsTotalSizeError",
99101
"UploadSessionCorruptedError",
100102
# vfolder
101103
"VFolderNotFoundError",

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@ def error_code(self) -> ErrorCode:
3232
)
3333

3434

35+
class UploadChunkExceedsTotalSizeError(BackendAIError, web.HTTPConflict):
36+
"""
37+
Raised when a PATCH chunk's offset+length would write past the declared
38+
``Upload-Length`` (409 Conflict). The Upload-Offset header itself is in
39+
range; the chunk's body simply overruns the remaining slot.
40+
"""
41+
42+
error_type = "https://api.backend.ai/probs/storage/upload-chunk-exceeds-total-size"
43+
error_title = "Upload Chunk Exceeds Total Size"
44+
45+
def error_code(self) -> ErrorCode:
46+
return ErrorCode(
47+
domain=ErrorDomain.STORAGE_PROXY,
48+
operation=ErrorOperation.UPDATE,
49+
error_detail=ErrorDetail.CONFLICT,
50+
)
51+
52+
3553
class UploadSessionCorruptedError(BackendAIError, web.HTTPInternalServerError):
3654
"""
3755
Raised when the on-disk upload session metadata cannot be parsed or is

0 commit comments

Comments
 (0)