Skip to content

Commit bc5da81

Browse files
kvzclaude
andcommitted
Generate the async chunk upload retry runtime
AsyncUploader's handwritten _do_request/_retry_or_cry recursion delegates to the generated async upload_chunk_with_retry, the same walked per-chunk retry procedure as the sync runtime with await at the patch and sleep effects. The retry decision helpers stay imported from the sync modules, and the legacy _retry_limit/_retry_delay_seconds/_should_retry helpers leave baseuploader with their last consumer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 685de6c commit bc5da81

3 files changed

Lines changed: 86 additions & 58 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Code generated from Transloadit API2 TUS protocol contracts; DO NOT EDIT.
2+
# If it looks wrong, please report the issue instead of editing this file by hand;
3+
# the source fix belongs in the protocol contract generator so all TUS clients stay in sync.
4+
5+
import asyncio
6+
7+
from tusclient.abort_generated import next_retry_attempt
8+
from tusclient.exceptions import TusCommunicationError, TusUploadFailed
9+
from tusclient.upload_chunks_generated import (
10+
effective_retry_attempt,
11+
should_schedule_upload_retry,
12+
)
13+
14+
15+
async def upload_chunk_with_retry(
16+
uploader,
17+
perform_patch_request,
18+
retry_delays,
19+
on_should_retry=None,
20+
):
21+
uploader._retried = 0
22+
offset_before_retry = uploader.offset
23+
start_offset = uploader.offset
24+
25+
if not uploader.upload_length_deferred:
26+
uploader.notify_progress(start_offset)
27+
28+
while True:
29+
error = None
30+
try:
31+
await perform_patch_request()
32+
except TusUploadFailed as patch_error:
33+
error = patch_error
34+
if error is None:
35+
if uploader.upload_length_deferred:
36+
uploader.notify_progress(start_offset)
37+
uploader.notify_progress(uploader.offset)
38+
uploader.notify_chunk_complete(uploader.offset - start_offset, uploader.offset)
39+
return
40+
41+
while error is not None:
42+
effective_attempt = effective_retry_attempt(
43+
uploader._retried,
44+
uploader.offset,
45+
offset_before_retry,
46+
)
47+
if not should_schedule_upload_retry(
48+
on_should_retry,
49+
error,
50+
effective_attempt,
51+
retry_delays,
52+
):
53+
raise error
54+
55+
delay_ms = retry_delays[effective_attempt]
56+
if delay_ms > 0:
57+
await asyncio.sleep(delay_ms / 1000)
58+
uploader._retried = next_retry_attempt(effective_attempt)
59+
offset_before_retry = uploader.offset
60+
error = None
61+
try:
62+
uploader.offset = uploader.get_offset()
63+
except TusCommunicationError as sync_error:
64+
error = sync_error
65+
continue

tusclient/uploader/baseuploader.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -256,12 +256,6 @@ def request_method_input_options(self):
256256
"override_patch_method": self.override_patch_method,
257257
}
258258

259-
def _retry_limit(self):
260-
if self.retry_delays is not None:
261-
return len(self.retry_delays)
262-
263-
return self.retries
264-
265259
def _upload_retry_delays_ms(self):
266260
"""The retry budget as the delays-indexed-by-attempt list (in milliseconds).
267261
@@ -274,18 +268,6 @@ def _upload_retry_delays_ms(self):
274268

275269
return [self.retry_delay * 1000] * self.retries
276270

277-
def _retry_delay_seconds(self, retry_attempt):
278-
if self.retry_delays is not None:
279-
return self.retry_delays[retry_attempt] / 1000
280-
281-
return self.retry_delay
282-
283-
def _should_retry(self, error, retry_attempt):
284-
if self.on_should_retry is None:
285-
return True
286-
287-
return bool(self.on_should_retry(error, retry_attempt))
288-
289271
def run_before_request(self, method, url, headers):
290272
if self.client is not None:
291273
self.client._set_current_uploader(self)

tusclient/uploader/uploader.py

Lines changed: 21 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
from tusclient.uploader.baseuploader import BaseUploader
1010

11+
from tusclient.async_upload_chunks_generated import (
12+
upload_chunk_with_retry as async_upload_chunk_with_retry,
13+
)
1114
from tusclient.detailed_error import (
1215
create_upload_request_error,
1316
create_upload_response_error,
@@ -396,26 +399,18 @@ async def upload_chunk(self):
396399
"""
397400
Upload chunk of file.
398401
"""
399-
self._retried = 0
400-
401402
# Ensure that we have a URL, as this is behavior we allowed previously.
402403
# See https://github.com/tus/tus-py-client/issues/82.
403404
if not self.url:
404405
self.set_url(await self.create_url())
405406
self.offset = 0
406407

407-
previous_offset = self.offset
408-
if not self.upload_length_deferred:
409-
self.notify_progress(previous_offset)
410-
await self._do_request()
411-
self.offset = int(self.request.response_headers.get("upload-offset"))
412-
if self.upload_length_deferred and self.request.stream_eof:
413-
self.file_size = self.offset
414-
self.stop_at = self.offset
415-
if self.upload_length_deferred:
416-
self.notify_progress(previous_offset)
417-
self.notify_progress(self.offset)
418-
self.notify_chunk_complete(self.offset - previous_offset, self.offset)
408+
await async_upload_chunk_with_retry(
409+
self,
410+
self._perform_patch_request,
411+
self._upload_retry_delays_ms(),
412+
on_should_retry=self.on_should_retry,
413+
)
419414
self.remove_url_on_success()
420415

421416
async def create_url(self):
@@ -459,30 +454,16 @@ async def create_url(self):
459454
raise TusUploadAborted()
460455
raise TusCommunicationError(error)
461456

462-
async def _do_request(self):
457+
async def _perform_patch_request(self):
458+
"""Send one chunk PATCH and absorb its accepted state on success.
459+
460+
The retry algorithm around this transport step lives in the generated
461+
``upload_chunk_with_retry`` (see tusclient/async_upload_chunks_generated.py).
462+
"""
463463
self.request = AsyncTusRequest(self)
464-
try:
465-
await self.request.perform()
466-
_verify_upload(self.request)
467-
except TusUploadFailed as error:
468-
await self._retry_or_cry(error)
469-
470-
async def _retry_or_cry(self, error):
471-
retry_attempt = self._retried
472-
if self._retry_limit() <= retry_attempt:
473-
raise error
474-
if not self._should_retry(error, retry_attempt):
475-
raise error
476-
477-
await asyncio.sleep(self._retry_delay_seconds(retry_attempt))
478-
self._retried += 1
479-
previous_offset = self.offset
480-
try:
481-
recovered_offset = self.get_offset()
482-
except TusCommunicationError as err:
483-
await self._retry_or_cry(err)
484-
else:
485-
if recovered_offset > previous_offset:
486-
self._retried = 0
487-
self.offset = recovered_offset
488-
await self._do_request()
464+
await self.request.perform()
465+
_verify_upload(self.request)
466+
self.offset = int(self.request.response_headers.get("upload-offset"))
467+
if self.upload_length_deferred and self.request.stream_eof:
468+
self.file_size = self.offset
469+
self.stop_at = self.offset

0 commit comments

Comments
 (0)