Repository navigation
storage: upload_chunks_concurrently ignores its timeout argument #18764
Description
Determine this is the right repository
- I determined this is the correct repository in which to report this bug.
Summary of the issue
transfer_manager.upload_chunks_concurrently takes a timeout argument and documents it as the time to wait for the server response, but it never passes it on. The XML multipart requests it makes (initiate, each part upload, finalize, and cancel when something fails) always run with the default (61, 60), so a caller cannot make a stuck upload fail sooner or give a slow link more time.
Expected: every request made by upload_chunks_concurrently uses the timeout the caller passed.
Actual: every request uses (61, 60) no matter what the caller passes.
API client name and version
google-cloud-storage 3.17.1 (main at 57366ee)
Reproduction steps: code
file: repro.py
"""Show which timeout each XML MPU request gets from upload_chunks_concurrently.
Runs the real transfer_manager and XML MPU code with a transport that records
the timeout of every request and answers like Cloud Storage would. No network.
"""
import os
import tempfile
import requests
from google.auth.credentials import AnonymousCredentials
from google.cloud.storage import Client, transfer_manager
INITIATE_BODY = (
'<?xml version="1.0" encoding="UTF-8"?>'
'<InitiateMultipartUploadResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">'
"<UploadId>upload-1</UploadId></InitiateMultipartUploadResult>"
)
class RecordingTransport(requests.Session):
is_mtls = False
def __init__(self):
super().__init__()
self.calls = []
def request(self, method, url, data=None, headers=None, timeout=None, **kwargs):
query = url.split("?", 1)[1] if "?" in url else ""
if method == "POST" and query == "uploads":
step, body, extra = "initiate", INITIATE_BODY, {}
elif method == "PUT":
step, body, extra = "upload part", "", {"ETag": '"etag-1"'}
elif method == "POST":
step, body, extra = "finalize", "", {}
else:
step, body, extra = "cancel", "", {}
self.calls.append((step, timeout))
response = requests.Response()
response.status_code = 204 if step == "cancel" else 200
response._content = body.encode()
response.headers.update(extra)
response.url = url
return response
transport = RecordingTransport()
client = Client(project="demo", credentials=AnonymousCredentials(), _http=transport)
blob = client.bucket("demo-bucket").blob("big-file.bin")
with tempfile.NamedTemporaryFile(delete=False) as handle:
handle.write(b"x" * 10)
try:
transfer_manager.upload_chunks_concurrently(
handle.name,
blob,
chunk_size=5,
worker_type=transfer_manager.THREAD,
checksum=None,
timeout=5,
)
except Exception as error:
print(f"raised {type(error).__name__}: {error}")
finally:
os.unlink(handle.name)
for step, timeout in transport.calls:
print(f"{step:12} timeout={timeout}")Reproduction steps: supporting files
None. The script answers the requests itself instead of using the network.
Reproduction steps: actual results
initiate timeout=(61, 60)
upload part timeout=(61, 60)
upload part timeout=(61, 60)
finalize timeout=(61, 60)
Reproduction steps: expected results
initiate timeout=5
upload part timeout=5
upload part timeout=5
finalize timeout=5
OS & version + platform
Linux 7.0.0
Python environment
Python 3.12.3
Python dependencies
google-api-core 2.42.0, google-auth 2.62.0, google-cloud-core 2.8.0, google-crc32c 1.9.0, requests 2.34.2
Additional context
XMLMPUContainer.initiate, finalize and cancel and XMLMPUPart.upload already accept timeout, so the fix is to pass it through, including into the _upload_part worker. I'm happy to send a PR for it.
Activity
fix(storage): honor timeout in upload_chunks_concurrently