Skip to content

fix(client): non-blocking fingerprint + immediate stop_event cancel (async)#2

Merged
injaeryou merged 3 commits into
mainfrom
fix/async-uploader-cancel-and-fingerprint
Jun 29, 2026
Merged

fix(client): non-blocking fingerprint + immediate stop_event cancel (async)#2
injaeryou merged 3 commits into
mainfrom
fix/async-uploader-cancel-and-fingerprint

Conversation

@injaeryou

Copy link
Copy Markdown
Owner

Follow-up to #1, addressing the actionable gemini-code-assist comments.

Changes

  • fingerprint off the event loopAsyncTusClient now computes the SHA-256 fingerprint via asyncio.to_thread, so hashing a large file no longer blocks the loop (only runs when store_url is set).
  • immediate stop_event cancellationAsyncUploader now checks stop_event at the top of upload_chunk and inside the upload loop. Previously it was only honored during the retry backoff wait, so with max_retries=0 (default) or while chunks kept succeeding, cancellation was ignored.
  • Added test_async_uploader_stop_event_cancels_without_retries.

Not addressed (intentional)

  • cleanup lock "deadlock" (critical) — not reproducible: _last_cleanup is updated before the await, so the double-checked guard makes concurrent requests skip the lock entirely in a single event loop. No change.
  • BytesIO slice OOM (medium) — pre-existing behavior mirrored from the sync client (parallel.py); a streaming FileSlice is a larger change for both paths, out of scope here.

TUS compliance

Client-side only. No wire bytes, headers, status codes, or Upload-Offset semantics change. A stop_event cancellation raises before sending a PATCH, leaving the partial upload resumable per the protocol.

Test result

  • pytest — 495 passed, 4 skipped
  • ruff check / ruff format --check / ty check clean

🤖 Generated with Claude Code

injaeryou and others added 3 commits June 29, 2026 21:27
…async)

Address gemini-code-assist review on #1:

- Run the SHA-256 fingerprint via asyncio.to_thread so a large file's
  hashing no longer blocks the event loop (only computed when store_url
  is set).
- Check stop_event at the top of AsyncUploader.upload_chunk and in the
  upload loop, so cancellation works even with retries disabled
  (max_retries=0) or while chunks keep succeeding — previously it was
  only honored during the retry backoff wait.

Client-side only; no wire/header/offset behavior changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rint

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The async dispatch held a threading.Lock across `await
cleanup_expired_uploads_async()`. With `cleanup_interval <= 0` the
double-checked guard always passes, so a second concurrent request would
call `acquire()` and block the event loop thread — a hard deadlock, since
the awaiting coroutine can never resume to release it.

Replace the lock with a non-blocking `_cleanup_running` flag on the async
path only; the sync path keeps the lock (no await, lock is correct there).
Single event loop => a plain flag is enough to prevent overlapping runs.

Regression test runs the loop in a worker thread and joins with a timeout,
since a frozen loop can't honor an in-loop asyncio.wait_for.

Addresses gemini-code-assist critical comment on #1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@injaeryou
injaeryou merged commit 7659306 into main Jun 29, 2026
10 checks passed
@injaeryou
injaeryou deleted the fix/async-uploader-cancel-and-fingerprint branch June 29, 2026 13:13
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