Fix stream_write to properly raise an exception on failure, instead of just silently failing
This was causing problems for customers using georeplication over unstable storage engines Also adds tests for stream_write and copy, to ensure we detect failure
This commit is contained in:
parent
a048ff3633
commit
3a0adfcb11
3 changed files with 59 additions and 18 deletions
|
@ -182,12 +182,28 @@ class _CloudStorage(BaseStorageV2):
|
|||
**self._upload_params)
|
||||
|
||||
def stream_write(self, path, fp, content_type=None, content_encoding=None):
|
||||
self._stream_write_internal(path, fp, content_type, content_encoding)
|
||||
""" Writes the data found in the file-like stream tot he given path. Raises an IOError
|
||||
if the write fails.
|
||||
"""
|
||||
_, write_error = self._stream_write_internal(path, fp, content_type, content_encoding)
|
||||
if write_error is not None:
|
||||
logger.error('Error when trying to stream_write path `%s`: %s', path, write_error)
|
||||
raise IOError('Exception when trying to stream_write path')
|
||||
|
||||
def _stream_write_internal(self, path, fp, content_type=None, content_encoding=None,
|
||||
cancel_on_error=True, size=filelike.READ_UNTIL_END):
|
||||
""" Writes the data found in the file-like stream to the given path, with optional limit
|
||||
on size. Note that this method returns a *tuple* of (bytes_written, write_error) and should
|
||||
*not* raise an exception (such as IOError) if a problem uploading occurred. ALWAYS check
|
||||
the returned tuple on calls to this method.
|
||||
"""
|
||||
write_error = None
|
||||
mp = self.__initiate_multipart_upload(path, content_type, content_encoding)
|
||||
|
||||
try:
|
||||
mp = self.__initiate_multipart_upload(path, content_type, content_encoding)
|
||||
except S3ResponseError as e:
|
||||
logger.exception('Exception when initiating multipart upload')
|
||||
return 0, e
|
||||
|
||||
# We are going to reuse this but be VERY careful to only read the number of bytes written to it
|
||||
buf = StringIO.StringIO()
|
||||
|
@ -211,7 +227,7 @@ class _CloudStorage(BaseStorageV2):
|
|||
mp.upload_part_from_file(buf, num_part, size=bytes_staged)
|
||||
total_bytes_written += bytes_staged
|
||||
num_part += 1
|
||||
except IOError as e:
|
||||
except (S3ResponseError, IOError) as e:
|
||||
logger.warn('Error when writing to stream in stream_write_internal at path %s: %s', path, e)
|
||||
write_error = e
|
||||
|
||||
|
@ -219,7 +235,11 @@ class _CloudStorage(BaseStorageV2):
|
|||
self._context.metric_queue.multipart_upload_end.Inc(labelvalues=['failure'])
|
||||
|
||||
if cancel_on_error:
|
||||
mp.cancel_upload()
|
||||
try:
|
||||
mp.cancel_upload()
|
||||
except (S3ResponseError, IOError):
|
||||
logger.exception('Could not cancel upload')
|
||||
|
||||
return 0, write_error
|
||||
else:
|
||||
break
|
||||
|
@ -263,6 +283,7 @@ class _CloudStorage(BaseStorageV2):
|
|||
return k.etag[1:-1][:7]
|
||||
|
||||
def copy_to(self, destination, path):
|
||||
""" Copies the given path from this storage to the destination storage. """
|
||||
self._initialize_cloud_conn()
|
||||
|
||||
# First try to copy directly via boto, but only if the storages are the
|
||||
|
@ -527,6 +548,11 @@ class GoogleCloudStorage(_CloudStorage):
|
|||
|
||||
def _stream_write_internal(self, path, fp, content_type=None, content_encoding=None,
|
||||
cancel_on_error=True, size=filelike.READ_UNTIL_END):
|
||||
""" Writes the data found in the file-like stream to the given path, with optional limit
|
||||
on size. Note that this method returns a *tuple* of (bytes_written, write_error) and should
|
||||
*not* raise an exception (such as IOError) if a problem uploading occurred. ALWAYS check
|
||||
the returned tuple on calls to this method.
|
||||
"""
|
||||
# Minimum size of upload part size on S3 is 5MB
|
||||
self._initialize_cloud_conn()
|
||||
path = self._init_path(path)
|
||||
|
|
Reference in a new issue