Skip to content

feat(storage): stream resumable file uploads - #59

Merged
swkeever merged 5 commits into
mainfrom
skeever/storage-upload-streaming
Sep 2, 2026
Merged

swkeever merged 5 commits into
mainfrom
skeever/storage-upload-streaming

Conversation

@swkeever

@swkeever swkeever commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Summary:

  • accept binary file-like inputs in upload_resumable
  • stream seekable files in server-selected chunks
  • spool non-seekable inputs to a temporary file with bounded reads
  • preserve existing bytes behavior and cleanup semantics

Scope:
Python facade-only input handling. No OpenAPI or Hosting behavior changes.

Verification:

  • OpenAPI provenance check
  • Ruff and formatting
  • mypy and Pyright
  • 259 unit tests
  • Behave contract dry run
  • package build
  • Hosting SDK contract guards

swkeever commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-02T17:09:58.231783Z 2eb8e5f Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75b6a915c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/volcano_sdk/storage.py Outdated
session_id=session.session_id,
part_number=part_index + 1,
data=data[offset : offset + session.part_size],
data=source.read(session.part_size),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fill each upload part before advancing

When a seekable binary stream returns fewer than the requested bytes before EOF—which is valid for file-like objects—this advances to the next part number after that short read. The resulting non-final part can violate the API's minimum part-size requirement, and the fixed total_parts loop may stop while unread data remains, causing the upload to fail or assemble incomplete content. Accumulate reads until session.part_size bytes have been collected or true EOF is reached.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bd90802. Part reads now accumulate until the server-selected size is filled or the source reaches EOF, with regression coverage for legal short reads.

Comment thread src/volcano_sdk/storage.py Outdated
Comment on lines +219 to +220
while chunk := source.read(_UPLOAD_SPOOL_READ_SIZE):
target.write(chunk)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Distinguish unavailable nonblocking data from EOF

For a non-seekable source backed by a nonblocking binary stream, read(size) may return None when no data is currently available, but this truthiness loop treats that result as EOF. If it occurs after reading a prefix, the SDK creates and completes a session containing only that prefix even though more bytes may arrive, silently truncating the upload; distinguish b"" from None and either wait, retry, or raise instead of finalizing the spool.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bd90802. A None read now raises BlockingIOError instead of being treated as EOF, preventing silent truncation; the behavior has focused regression coverage.

raise
return self.complete_upload_session(path, session_id=session.session_id)
"""Upload bytes or a binary stream through a resumable session."""
with _resumable_upload_source(data) as (source, total_size):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate upload preconditions before spooling the source

For a non-seekable input, entering this context drains the entire source into a temporary file before create_upload_session() validates the path or checks for an active session. Consequently, an invalid path or unauthenticated call with a large or live stream can consume unbounded disk space or block indefinitely before raising an error that was knowable immediately; validate those local preconditions before entering the spooling context.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed the valid local portion in bd90802. Path and active-session authentication are validated before a non-seekable source is spooled. The server-side active-upload check remains in createUploadSession because total_size is required by that wire operation and is unknowable until spooling completes.

@swkeever

swkeever commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd90802cee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/volcano_sdk/storage.py Outdated
)
try:
self._upload_session_parts(path, source, session)
except (OSError, TypeError, ValueError, VolcanoError):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Abort sessions when stream readers raise other exceptions

For a custom BinaryIO whose read() raises an exception outside this allowlist (for example, RuntimeError) after the upload session has been created, upload_resumable() propagates the error without calling _abort_failed_upload(). This leaves the server-side session and any uploaded parts active even though the convenience operation has failed; catch arbitrary Exception around the part-upload loop while still re-raising the original error.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6d2f4b8. A try/finally success guard now aborts the session for any exception raised while reading or uploading parts, without catching or replacing the original exception.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: the fix is in 5ef28e9 (the prior reply contained a mistyped short SHA).

Comment on lines +207 to +208
if not source.seekable():
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat readers without seekable() as non-seekable

For a non-seekable binary file-like object that exposes bounded read() but does not inherit IOBase and therefore has no seekable() method, this unconditional probe raises AttributeError instead of using the advertised spooling path. This affects common read-only stream wrappers such as HTTP response bodies; treat an absent seekability probe as non-seekable so these sources can be spooled.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6d2f4b8. Missing seekability probes are treated as non-seekable and use the bounded spooling path, with regression coverage for a duck-typed read-only stream.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: the fix is in 5ef28e9 (the prior reply contained a mistyped short SHA).

@swkeever

swkeever commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5ef28e9b1b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

upload_succeeded = True
finally:
if not upload_succeeded:
self._abort_failed_upload(path, session.session_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the upload error when abort cleanup fails

When a stream read fails and the best-effort abort raises something other than VolcanoError—for example, _session_token() raises RuntimeError after a concurrent sign-out—this finally block replaces the original reader error with the cleanup error because _abort_failed_upload() suppresses only VolcanoError. The fresh evidence in this revision is the new unconditional cleanup path for arbitrary reader exceptions; suppress ordinary abort-time exceptions so the original upload failure remains observable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 11bf76a. Best-effort abort now suppresses ordinary cleanup exceptions, and regression coverage proves the original reader failure remains observable when abort itself raises.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: the fix is in 4f8f7d2 (the prior reply contained a mistyped short SHA).

@swkeever

swkeever commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4f8f7d26b1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/volcano_sdk/storage.py Outdated
position = source.tell()
try:
source.seek(0, SEEK_END)
return source.tell() - position

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clamp the remaining stream size to zero

When a valid seekable stream is positioned beyond EOF (for example, BytesIO(b"a").seek(2)), the end position is smaller than the current position, so this returns a negative size. upload_resumable() then sends that negative total_size when creating the session instead of treating the stream's readable remainder as an empty upload; clamp the difference to zero.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2eb8e5f. Remaining readable size is now clamped to zero when the cursor is beyond EOF, with focused regression coverage.

Comment thread src/volcano_sdk/storage.py Outdated
Comment on lines +212 to +215
finally:
source.seek(position)
except (AttributeError, OSError, ValueError):
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not spool after failing to restore the stream position

When an ostensibly seekable stream reaches EOF during the size probe but seek(position) then raises OSError or ValueError—for example, a remote range-backed stream whose restore operation fails—the outer handler converts that restoration failure into the non-seekable fallback even though the cursor is still at EOF. The subsequent spool can therefore create and complete an empty upload instead of surfacing the I/O failure; only fall back when the original position is known to be intact, and otherwise propagate the restoration error.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2eb8e5f. The size probe now restores the original cursor in a finally block; restoration failures propagate, and fallback spooling occurs only after a successful restore.

@swkeever

swkeever commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: 2eb8e5f7c1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@swkeever
swkeever marked this pull request as ready for review September 2, 2026 17:10
@swkeever
swkeever merged commit fa8062c into main Sep 2, 2026
2 checks passed
@swkeever
swkeever deleted the skeever/storage-upload-streaming branch September 2, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant