client.py: Reset of streams during upload retries
This commit is contained in:
commit
6024b39314
@ -696,7 +696,7 @@ class Connection(object):
|
|||||||
"""Convenience class to make requests that will also retry the request"""
|
"""Convenience class to make requests that will also retry the request"""
|
||||||
|
|
||||||
def __init__(self, authurl, user, key, retries=5, preauthurl=None,
|
def __init__(self, authurl, user, key, retries=5, preauthurl=None,
|
||||||
preauthtoken=None, snet=False):
|
preauthtoken=None, snet=False, starting_backoff=1):
|
||||||
"""
|
"""
|
||||||
:param authurl: authenitcation URL
|
:param authurl: authenitcation URL
|
||||||
:param user: user name to authenticate as
|
:param user: user name to authenticate as
|
||||||
@ -716,6 +716,7 @@ class Connection(object):
|
|||||||
self.token = preauthtoken
|
self.token = preauthtoken
|
||||||
self.attempts = 0
|
self.attempts = 0
|
||||||
self.snet = snet
|
self.snet = snet
|
||||||
|
self.starting_backoff = starting_backoff
|
||||||
|
|
||||||
def get_auth(self):
|
def get_auth(self):
|
||||||
return get_auth(self.authurl, self.user, self.key, snet=self.snet)
|
return get_auth(self.authurl, self.user, self.key, snet=self.snet)
|
||||||
@ -723,9 +724,9 @@ class Connection(object):
|
|||||||
def http_connection(self):
|
def http_connection(self):
|
||||||
return http_connection(self.url)
|
return http_connection(self.url)
|
||||||
|
|
||||||
def _retry(self, func, *args, **kwargs):
|
def _retry(self, reset_func, func, *args, **kwargs):
|
||||||
self.attempts = 0
|
self.attempts = 0
|
||||||
backoff = 1
|
backoff = self.starting_backoff
|
||||||
while self.attempts <= self.retries:
|
while self.attempts <= self.retries:
|
||||||
self.attempts += 1
|
self.attempts += 1
|
||||||
try:
|
try:
|
||||||
@ -754,10 +755,12 @@ class Connection(object):
|
|||||||
raise
|
raise
|
||||||
sleep(backoff)
|
sleep(backoff)
|
||||||
backoff *= 2
|
backoff *= 2
|
||||||
|
if reset_func:
|
||||||
|
reset_func(func, *args, **kwargs)
|
||||||
|
|
||||||
def head_account(self):
|
def head_account(self):
|
||||||
"""Wrapper for :func:`head_account`"""
|
"""Wrapper for :func:`head_account`"""
|
||||||
return self._retry(head_account)
|
return self._retry(None, head_account)
|
||||||
|
|
||||||
def get_account(self, marker=None, limit=None, prefix=None,
|
def get_account(self, marker=None, limit=None, prefix=None,
|
||||||
full_listing=False):
|
full_listing=False):
|
||||||
@ -765,16 +768,16 @@ class Connection(object):
|
|||||||
# TODO(unknown): With full_listing=True this will restart the entire
|
# TODO(unknown): With full_listing=True this will restart the entire
|
||||||
# listing with each retry. Need to make a better version that just
|
# listing with each retry. Need to make a better version that just
|
||||||
# retries where it left off.
|
# retries where it left off.
|
||||||
return self._retry(get_account, marker=marker, limit=limit,
|
return self._retry(None, get_account, marker=marker, limit=limit,
|
||||||
prefix=prefix, full_listing=full_listing)
|
prefix=prefix, full_listing=full_listing)
|
||||||
|
|
||||||
def post_account(self, headers):
|
def post_account(self, headers):
|
||||||
"""Wrapper for :func:`post_account`"""
|
"""Wrapper for :func:`post_account`"""
|
||||||
return self._retry(post_account, headers)
|
return self._retry(None, post_account, headers)
|
||||||
|
|
||||||
def head_container(self, container):
|
def head_container(self, container):
|
||||||
"""Wrapper for :func:`head_container`"""
|
"""Wrapper for :func:`head_container`"""
|
||||||
return self._retry(head_container, container)
|
return self._retry(None, head_container, container)
|
||||||
|
|
||||||
def get_container(self, container, marker=None, limit=None, prefix=None,
|
def get_container(self, container, marker=None, limit=None, prefix=None,
|
||||||
delimiter=None, full_listing=False):
|
delimiter=None, full_listing=False):
|
||||||
@ -782,43 +785,55 @@ class Connection(object):
|
|||||||
# TODO(unknown): With full_listing=True this will restart the entire
|
# TODO(unknown): With full_listing=True this will restart the entire
|
||||||
# listing with each retry. Need to make a better version that just
|
# listing with each retry. Need to make a better version that just
|
||||||
# retries where it left off.
|
# retries where it left off.
|
||||||
return self._retry(get_container, container, marker=marker,
|
return self._retry(None, get_container, container, marker=marker,
|
||||||
limit=limit, prefix=prefix, delimiter=delimiter,
|
limit=limit, prefix=prefix, delimiter=delimiter,
|
||||||
full_listing=full_listing)
|
full_listing=full_listing)
|
||||||
|
|
||||||
def put_container(self, container, headers=None):
|
def put_container(self, container, headers=None):
|
||||||
"""Wrapper for :func:`put_container`"""
|
"""Wrapper for :func:`put_container`"""
|
||||||
return self._retry(put_container, container, headers=headers)
|
return self._retry(None, put_container, container, headers=headers)
|
||||||
|
|
||||||
def post_container(self, container, headers):
|
def post_container(self, container, headers):
|
||||||
"""Wrapper for :func:`post_container`"""
|
"""Wrapper for :func:`post_container`"""
|
||||||
return self._retry(post_container, container, headers)
|
return self._retry(None, post_container, container, headers)
|
||||||
|
|
||||||
def delete_container(self, container):
|
def delete_container(self, container):
|
||||||
"""Wrapper for :func:`delete_container`"""
|
"""Wrapper for :func:`delete_container`"""
|
||||||
return self._retry(delete_container, container)
|
return self._retry(None, delete_container, container)
|
||||||
|
|
||||||
def head_object(self, container, obj):
|
def head_object(self, container, obj):
|
||||||
"""Wrapper for :func:`head_object`"""
|
"""Wrapper for :func:`head_object`"""
|
||||||
return self._retry(head_object, container, obj)
|
return self._retry(None, head_object, container, obj)
|
||||||
|
|
||||||
def get_object(self, container, obj, resp_chunk_size=None):
|
def get_object(self, container, obj, resp_chunk_size=None):
|
||||||
"""Wrapper for :func:`get_object`"""
|
"""Wrapper for :func:`get_object`"""
|
||||||
return self._retry(get_object, container, obj,
|
return self._retry(None, get_object, container, obj,
|
||||||
resp_chunk_size=resp_chunk_size)
|
resp_chunk_size=resp_chunk_size)
|
||||||
|
|
||||||
def put_object(self, container, obj, contents, content_length=None,
|
def put_object(self, container, obj, contents, content_length=None,
|
||||||
etag=None, chunk_size=65536, content_type=None,
|
etag=None, chunk_size=65536, content_type=None,
|
||||||
headers=None):
|
headers=None):
|
||||||
"""Wrapper for :func:`put_object`"""
|
"""Wrapper for :func:`put_object`"""
|
||||||
return self._retry(put_object, container, obj, contents,
|
|
||||||
|
def _default_reset(*args, **kwargs):
|
||||||
|
raise ClientException('put_object(%r, %r, ...) failure and no '
|
||||||
|
'ability to reset contents for reupload.' % (container, obj))
|
||||||
|
|
||||||
|
reset_func = _default_reset
|
||||||
|
tell = getattr(contents, 'tell', None)
|
||||||
|
seek = getattr(contents, 'seek', None)
|
||||||
|
if tell and seek:
|
||||||
|
orig_pos = tell()
|
||||||
|
reset_func = lambda *a, **k: seek(orig_pos)
|
||||||
|
|
||||||
|
return self._retry(reset_func, put_object, container, obj, contents,
|
||||||
content_length=content_length, etag=etag, chunk_size=chunk_size,
|
content_length=content_length, etag=etag, chunk_size=chunk_size,
|
||||||
content_type=content_type, headers=headers)
|
content_type=content_type, headers=headers)
|
||||||
|
|
||||||
def post_object(self, container, obj, headers):
|
def post_object(self, container, obj, headers):
|
||||||
"""Wrapper for :func:`post_object`"""
|
"""Wrapper for :func:`post_object`"""
|
||||||
return self._retry(post_object, container, obj, headers)
|
return self._retry(None, post_object, container, obj, headers)
|
||||||
|
|
||||||
def delete_object(self, container, obj):
|
def delete_object(self, container, obj):
|
||||||
"""Wrapper for :func:`delete_object`"""
|
"""Wrapper for :func:`delete_object`"""
|
||||||
return self._retry(delete_object, container, obj)
|
return self._retry(None, delete_object, container, obj)
|
||||||
|
@ -14,7 +14,10 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
# TODO: More tests
|
# TODO: More tests
|
||||||
|
import socket
|
||||||
import unittest
|
import unittest
|
||||||
|
from StringIO import StringIO
|
||||||
|
from urlparse import urlparse
|
||||||
|
|
||||||
# TODO: mock http connection class with more control over headers
|
# TODO: mock http connection class with more control over headers
|
||||||
from test.unit.proxy.test_server import fake_http_connect
|
from test.unit.proxy.test_server import fake_http_connect
|
||||||
@ -377,5 +380,97 @@ class TestConnection(MockHttpTest):
|
|||||||
self.assertEquals(conn.url, 'http://www.new.com')
|
self.assertEquals(conn.url, 'http://www.new.com')
|
||||||
self.assertEquals(conn.token, 'new')
|
self.assertEquals(conn.token, 'new')
|
||||||
|
|
||||||
|
def test_reset_stream(self):
|
||||||
|
|
||||||
|
class LocalContents(object):
|
||||||
|
|
||||||
|
def __init__(self, tell_value=0):
|
||||||
|
self.already_read = False
|
||||||
|
self.seeks = []
|
||||||
|
self.tell_value = tell_value
|
||||||
|
|
||||||
|
def tell(self):
|
||||||
|
return self.tell_value
|
||||||
|
|
||||||
|
def seek(self, position):
|
||||||
|
self.seeks.append(position)
|
||||||
|
self.already_read = False
|
||||||
|
|
||||||
|
def read(self, size=-1):
|
||||||
|
if self.already_read:
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
self.already_read = True
|
||||||
|
return 'abcdef'
|
||||||
|
|
||||||
|
class LocalConnection(object):
|
||||||
|
|
||||||
|
def putrequest(self, *args, **kwargs):
|
||||||
|
return
|
||||||
|
|
||||||
|
def putheader(self, *args, **kwargs):
|
||||||
|
return
|
||||||
|
|
||||||
|
def endheaders(self, *args, **kwargs):
|
||||||
|
return
|
||||||
|
|
||||||
|
def send(self, *args, **kwargs):
|
||||||
|
raise socket.error('oops')
|
||||||
|
|
||||||
|
def request(self, *args, **kwargs):
|
||||||
|
return
|
||||||
|
|
||||||
|
def getresponse(self, *args, **kwargs):
|
||||||
|
self.status = 200
|
||||||
|
return self
|
||||||
|
|
||||||
|
def getheader(self, *args, **kwargs):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def read(self, *args, **kwargs):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def local_http_connection(url):
|
||||||
|
parsed = urlparse(url)
|
||||||
|
return parsed, LocalConnection()
|
||||||
|
|
||||||
|
orig_conn = c.http_connection
|
||||||
|
try:
|
||||||
|
c.http_connection = local_http_connection
|
||||||
|
conn = c.Connection('http://www.example.com', 'asdf', 'asdf',
|
||||||
|
retries=1, starting_backoff=.0001)
|
||||||
|
|
||||||
|
contents = LocalContents()
|
||||||
|
exc = None
|
||||||
|
try:
|
||||||
|
conn.put_object('c', 'o', contents)
|
||||||
|
except socket.error, err:
|
||||||
|
exc = err
|
||||||
|
self.assertEquals(contents.seeks, [0])
|
||||||
|
self.assertEquals(str(exc), 'oops')
|
||||||
|
|
||||||
|
contents = LocalContents(tell_value=123)
|
||||||
|
exc = None
|
||||||
|
try:
|
||||||
|
conn.put_object('c', 'o', contents)
|
||||||
|
except socket.error, err:
|
||||||
|
exc = err
|
||||||
|
self.assertEquals(contents.seeks, [123])
|
||||||
|
self.assertEquals(str(exc), 'oops')
|
||||||
|
|
||||||
|
contents = LocalContents()
|
||||||
|
contents.tell = None
|
||||||
|
exc = None
|
||||||
|
try:
|
||||||
|
conn.put_object('c', 'o', contents)
|
||||||
|
except c.ClientException, err:
|
||||||
|
exc = err
|
||||||
|
self.assertEquals(contents.seeks, [])
|
||||||
|
self.assertEquals(str(exc), "put_object('c', 'o', ...) failure "
|
||||||
|
"and no ability to reset contents for reupload.")
|
||||||
|
finally:
|
||||||
|
c.http_connection = orig_conn
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user