Skip to content

Commit 51181ab

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 099ec9d commit 51181ab

4 files changed

Lines changed: 116 additions & 36 deletions

File tree

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

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,15 @@
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
)
@@ -99,7 +103,7 @@ class UploadTokenData(TypedDict):
99103
volume: str
100104
vfid: VFolderID
101105
relpath: str
102-
session: str
106+
session: TusSessionId
103107
size: int
104108

105109

@@ -349,10 +353,11 @@ class Params(TypedDict):
349353
session_id=token_data["session"],
350354
total_size=int(token_data["size"]),
351355
valkey_client=ctx.valkey_tus_client,
356+
lock_factory=ctx.tus_lock_factory,
352357
)
353358
)
354359
state = await session.read_state()
355-
headers = _tus_response_headers(
360+
headers = _prepare_tus_session_headers(
356361
upload_offset=state.committed_offset,
357362
upload_length=int(token_data["size"]),
358363
)
@@ -428,31 +433,30 @@ class Params(TypedDict):
428433
session_id=token_data["session"],
429434
total_size=total_size,
430435
valkey_client=ctx.valkey_tus_client,
436+
lock_factory=ctx.tus_lock_factory,
431437
)
432438
)
433439
await session.ensure_initialized()
434440

435441
upload_stream = TusChunkUploadStreamReader(
436442
request.content, request.content_type, DEFAULT_CHUNK_SIZE
437443
)
438-
temp_chunk, length, sha256 = await session.write_temp_chunk(
439-
client_offset, upload_stream
440-
)
444+
written = await session.write_temp_chunk(client_offset, upload_stream)
441445
try:
442-
if client_offset + length > total_size:
443-
raise UploadOffsetMismatchError(
444-
f"Chunk at offset {client_offset} with length {length} "
446+
if client_offset + written.length > total_size:
447+
raise UploadChunkExceedsTotalSizeError(
448+
f"Chunk at offset {client_offset} with length {written.length} "
445449
f"exceeds declared size {total_size}"
446450
)
447451
acceptance = await session.commit_chunk(
448452
offset=client_offset,
449-
chunk_path=temp_chunk.path,
450-
length=length,
451-
sha256=sha256,
453+
chunk_path=written.path,
454+
length=written.length,
455+
sha256=written.sha256,
452456
)
453457
except BaseException:
454-
if temp_chunk.path.exists():
455-
await asyncio.to_thread(temp_chunk.path.unlink)
458+
if written.path.exists():
459+
await asyncio.to_thread(written.path.unlink)
456460
raise
457461

458462
state = acceptance.state
@@ -464,7 +468,7 @@ class Params(TypedDict):
464468
await session.assemble(target_path)
465469
await session.cleanup()
466470

467-
headers = _tus_response_headers(
471+
headers = _prepare_tus_session_headers(
468472
upload_offset=state.committed_offset,
469473
upload_length=total_size,
470474
)
@@ -478,7 +482,7 @@ def _resolve_tus_upload_session_dir(volume: AbstractVolume, token_data: UploadTo
478482
_TUS_HEADER_LIST = "Tus-Resumable, Upload-Length, Upload-Metadata, Upload-Offset, Content-Type"
479483

480484

481-
def _tus_response_headers(*, upload_offset: int, upload_length: int) -> dict[str, str]:
485+
def _prepare_tus_session_headers(*, upload_offset: int, upload_length: int) -> dict[str, str]:
482486
return {
483487
"Access-Control-Allow-Origin": "*",
484488
"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)