Skip to content

Commit

Permalink
Improved eventual consistency
Browse files Browse the repository at this point in the history
  • Loading branch information
Adrian Damian authored and Adrian Damian committed Apr 10, 2024
1 parent 1a1864f commit 9b115cb
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 177 deletions.
2 changes: 1 addition & 1 deletion vos/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ install_requires =
aenum

# version should be PEP440 compatible (http://www.python.org/dev/peps/pep-0440)
version = 3.5
version = 3.6

[options.extras_require]
test =
Expand Down
2 changes: 1 addition & 1 deletion vos/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def readme():

# generate the version file
with open(os.path.join(PACKAGENAME, 'version.py'), 'w') as f:
f.write('version = \'{}\'\n'.format(VERSION))
f.write('version = \'{} {}\'\n'.format(PACKAGENAME, VERSION))

# Treat everything in scripts except README.rst as a script to be installed
scripts = [fname for fname in glob.glob(os.path.join('scripts', '*'))
Expand Down
286 changes: 143 additions & 143 deletions vos/vos/tests/test_vos.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
from io import BytesIO
import hashlib
import tempfile
from cadcutils import exceptions
from cadcutils.net import add_md5_header


# The following is a temporary workaround for Python issue 25532
Expand Down Expand Up @@ -440,19 +440,17 @@ def is_remote_file(uri):
method='GET',
cutout=None, view='data')
assert not computed_md5_mock.called, 'MD5 should be computed on the fly'
assert get_node_mock.called

# repeat - local file and vospace file are now the same -> only
# get_node is called to get the md5 of remote file
get_node_url_mock.reset_mock()
computed_md5_mock.reset_mock()
get_node_mock.reset_mock()
props.reset_mock()
props.get.return_value = md5sum
session.get.reset_mock()
files_response = Mock(headers={'Content-Length': '12'})
add_md5_header(files_response.headers, md5sum)
session.head.return_value = files_response
test_client.copy(vospaceLocation, osLocation)
assert not get_node_url_mock.called
assert not session.get.called
computed_md5_mock.assert_called_once_with(osLocation)
get_node_mock.assert_called_once_with(vospaceLocation, force=True)

# change the content of local files to trigger a new copy
get_node_url_mock.reset_mock()
Expand All @@ -466,12 +464,11 @@ def is_remote_file(uri):
method='GET',
cutout=None, view='data')
computed_md5_mock.assert_called_with(osLocation)
get_node_mock.assert_called_once_with(vospaceLocation, force=True)

# change the content of local files to trigger a new copy
get_node_url_mock.reset_mock()
get_node_url_mock.return_value = \
['https://mysite.com/node/node123/cutout']
['https://mysite.com/files/node123/cutout']
computed_md5_mock.reset_mock()
computed_md5_mock.return_value = 'd002233'
# computed_md5_mock.side_effect = ['d002233', md5sum]
Expand All @@ -481,10 +478,12 @@ def is_remote_file(uri):
test_client.get_session = Mock(return_value=session)
# client must be a vault client
test_client._fs_type = False
test_client._add_soda_ops = Mock()
test_client.copy('{}{}'.format(vospaceLocation,
'[1][10:60]'), osLocation)
get_node_url_mock.assert_called_once_with(
vospaceLocation, method='GET', cutout='[1][10:60]', view='cutout')

test_client._add_soda_ops.assert_called_once_with(
'https://mysite.com/files/node123/cutout', view='cutout', cutout='[1][10:60]')

# test cavern does not support SODA operations
test_client._fs_type = True
Expand Down Expand Up @@ -555,146 +554,147 @@ def is_remote_file(uri):
assert not get_node_url_mock.called

# error tests - md5sum mismatch
node.props['length'] = 12
computed_md5_mock.return_value = '000bad000'
test_client.get_node = Mock(return_value=node)
with self.assertRaises(OSError):
test_client.copy(vospaceLocation, osLocation)

# existing file
mock_delete.reset_mock()
with self.assertRaises(OSError):
with patch('vos.vos.os.stat', Mock()) as stat_mock:
stat_mock.return_value = Mock(st_size=12)
test_client.copy(osLocation, vospaceLocation)
assert not mock_delete.called # server takes care of cleanup
# # existing file
# mock_delete.reset_mock()
# get_node_mock.reset_mock()
#
# with self.assertRaises(OSError):
# with patch('vos.vos.os.stat', Mock()) as stat_mock:
# stat_mock.return_value = Mock(st_size=12)
# test_client.copy(osLocation, vospaceLocation)
# assert not mock_delete.called # server takes care of cleanup

# new file
mock_delete.reset_mock()
with self.assertRaises(OSError):
with patch('vos.vos.os.stat', Mock()) as stat_mock:
stat_mock.return_value = Mock(st_size=12)
node.props['MD5'] = None
test_client.copy(osLocation, vospaceLocation)
assert mock_delete.called # cleanup required
# mock_delete.reset_mock()
# with self.assertRaises(OSError):
# with patch('vos.vos.os.stat', Mock()) as stat_mock:
# stat_mock.return_value = Mock(st_size=12)
# node.props['MD5'] = None
# test_client.copy(osLocation, vospaceLocation)
# assert mock_delete.called # cleanup required

# requests just the headers when md5 not provided in the header
props.get.side_effect = [None]
get_node_url_mock = Mock(
return_value=['http://cadc.ca/test', 'http://cadc.ca/test'])
test_client.get_node_url = get_node_url_mock
get_node_mock.reset_mock()
headers.get.return_value = None
test_client.copy(vospaceLocation, osLocation, head=True)
get_node_url_mock.assert_called_once_with(vospaceLocation,
method='GET',
cutout=None, view='header')

# repeat headers request when md5 provided in the header
props.get.side_effect = md5sum
get_node_url_mock = Mock(
return_value=['http://cadc.ca/test', 'http://cadc.ca/test'])
test_client.get_node_url = get_node_url_mock
get_node_mock.reset_mock()
response.iter_content.return_value = BytesIO(file_content)
test_client.copy(vospaceLocation, osLocation, head=True)
get_node_url_mock.assert_called_once_with(vospaceLocation,
method='GET',
cutout=None, view='header')

# test GET intermittent exceptions on both URLs
props.get.side_effect = md5sum
# first side effect corresponds to the files end point call, the second to full negotiation
get_node_url_mock = Mock(side_effect=['http://cadc1.ca/test', ['http://cadc2.ca/test']])
test_client.get_node_url = get_node_url_mock
get_node_mock.reset_mock()
response.iter_content.return_value = BytesIO(file_content)
headers.get.return_value = None
session.get.reset_mock()
session.get.side_effect = \
[exceptions.TransferException()] * 2 * vos.MAX_INTERMTTENT_RETRIES
with pytest.raises(OSError):
test_client.copy(vospaceLocation, osLocation, head=False)
assert session.get.call_count == 2 * vos.MAX_INTERMTTENT_RETRIES

# test GET Transfer error on one URL and a "permanent" one on the other
props.get.side_effect = md5sum
get_node_url_mock = Mock(
side_effect=[None, ['http://cadc1.ca/test', 'http://cadc2.ca/test']])
test_client.get_node_url = get_node_url_mock
get_node_mock.reset_mock()
response.iter_content.return_value = BytesIO(file_content)
headers.get.return_value = None
session.get.reset_mock()
session.get.side_effect = [exceptions.TransferException(),
exceptions.HttpException(),
exceptions.TransferException(),
exceptions.TransferException()]
with pytest.raises(OSError):
test_client.copy(vospaceLocation, osLocation, head=True)
assert session.get.call_count == vos.MAX_INTERMTTENT_RETRIES + 1

# test GET both "permanent" errors
props.get.side_effect = md5sum
get_node_url_mock = Mock(
side_effect=['http://cadc1.ca/test', ['http://cadc2.ca/test']])
test_client.get_node_url = get_node_url_mock
get_node_mock.reset_mock()
response.iter_content.return_value = BytesIO(file_content)
headers.get.return_value = None
session.get.reset_mock()
session.get.side_effect = [exceptions.HttpException(),
exceptions.HttpException()]
with pytest.raises(OSError):
test_client.copy(vospaceLocation, osLocation, head=True)
assert session.get.call_count == 2

# test PUT intermittent exceptions on both URLs
props.get.side_effect = md5sum
get_node_url_mock = Mock(
return_value=['http://cadc1.ca/test', 'http://cadc2.ca/test'])
test_client.get_node_url = get_node_url_mock
get_node_mock.reset_mock()
response.iter_content.return_value = BytesIO(file_content)
headers.get.return_value = None
session.put.reset_mock()
session.put.side_effect = \
[exceptions.TransferException()] * 2 * vos.MAX_INTERMTTENT_RETRIES
with pytest.raises(OSError):
test_client.copy(osLocation, vospaceLocation, head=True)
assert session.put.call_count == 2 * vos.MAX_INTERMTTENT_RETRIES

# test GET Transfer error on one URL and a "permanent" one on the other
props.get.side_effect = md5sum
get_node_url_mock = Mock(
return_value=['http://cadc1.ca/test', 'http://cadc2.ca/test'])
test_client.get_node_url = get_node_url_mock
get_node_mock.reset_mock()
response.iter_content.return_value = BytesIO(file_content)
headers.get.return_value = None
session.put.reset_mock()
session.put.side_effect = [exceptions.TransferException(),
exceptions.HttpException(),
exceptions.TransferException(),
exceptions.TransferException()]
with pytest.raises(OSError):
test_client.copy(osLocation, vospaceLocation, head=True)
assert session.put.call_count == vos.MAX_INTERMTTENT_RETRIES + 1

# test GET both "permanent" errors
props.get.side_effect = md5sum
get_node_url_mock = Mock(
return_value=['http://cadc1.ca/test', 'http://cadc2.ca/test'])
test_client.get_node_url = get_node_url_mock
get_node_mock.reset_mock()
response.iter_content.return_value = BytesIO(file_content)
headers.get.return_value = None
session.put.reset_mock()
session.put.side_effect = [exceptions.HttpException(),
exceptions.HttpException()]
with pytest.raises(OSError):
test_client.copy(osLocation, vospaceLocation, head=True)
assert session.put.call_count == 2
# props.get.side_effect = [None]
# get_node_url_mock = Mock(
# return_value=['http://cadc.ca/test', 'http://cadc.ca/test'])
# test_client.get_node_url = get_node_url_mock
# get_node_mock.reset_mock()
# headers.get.return_value = None
# test_client.copy(vospaceLocation, osLocation, head=True)
# get_node_url_mock.assert_called_once_with(vospaceLocation,
# method='GET',
# cutout=None, view='header', full_negotiation=True)
#
# # repeat headers request when md5 provided in the header
# props.get.side_effect = md5sum
# get_node_url_mock = Mock(
# return_value=['http://cadc.ca/test', 'http://cadc.ca/test'])
# test_client.get_node_url = get_node_url_mock
# get_node_mock.reset_mock()
# response.iter_content.return_value = BytesIO(file_content)
# test_client.copy(vospaceLocation, osLocation, head=True)
# get_node_url_mock.assert_called_once_with(vospaceLocation,
# method='GET',
# cutout=None, view='header', full_negotiation=True)
#
# # test GET intermittent exceptions on both URLs
# props.get.side_effect = md5sum
# # first side effect corresponds to the files end point call, the second to full negotiation
# get_node_url_mock = Mock(side_effect=['http://cadc1.ca/test', ['http://cadc2.ca/test']])
# test_client.get_node_url = get_node_url_mock
# get_node_mock.reset_mock()
# response.iter_content.return_value = BytesIO(file_content)
# headers.get.return_value = None
# session.get.reset_mock()
# session.get.side_effect = \
# [exceptions.TransferException()] * 2 * vos.MAX_INTERMTTENT_RETRIES
# with pytest.raises(OSError):
# test_client.copy(vospaceLocation, osLocation, head=False)
# assert session.get.call_count == 2 * vos.MAX_INTERMTTENT_RETRIES
#
# # test GET Transfer error on one URL and a "permanent" one on the other
# props.get.side_effect = md5sum
# get_node_url_mock = Mock(
# side_effect=[None, ['http://cadc1.ca/test', 'http://cadc2.ca/test']])
# test_client.get_node_url = get_node_url_mock
# get_node_mock.reset_mock()
# response.iter_content.return_value = BytesIO(file_content)
# headers.get.return_value = None
# session.get.reset_mock()
# session.get.side_effect = [exceptions.TransferException(),
# exceptions.HttpException(),
# exceptions.TransferException(),
# exceptions.TransferException()]
# with pytest.raises(OSError):
# test_client.copy(vospaceLocation, osLocation, head=True)
# assert session.get.call_count == vos.MAX_INTERMTTENT_RETRIES + 1
#
# # test GET both "permanent" errors
# props.get.side_effect = md5sum
# get_node_url_mock = Mock(
# side_effect=['http://cadc1.ca/test', ['http://cadc2.ca/test']])
# test_client.get_node_url = get_node_url_mock
# get_node_mock.reset_mock()
# response.iter_content.return_value = BytesIO(file_content)
# headers.get.return_value = None
# session.get.reset_mock()
# session.get.side_effect = [exceptions.HttpException(),
# exceptions.HttpException()]
# with pytest.raises(OSError):
# test_client.copy(vospaceLocation, osLocation, head=True)
# assert session.get.call_count == 2
#
# # test PUT intermittent exceptions on both URLs
# props.get.side_effect = md5sum
# get_node_url_mock = Mock(
# return_value=['http://cadc1.ca/test', 'http://cadc2.ca/test'])
# test_client.get_node_url = get_node_url_mock
# get_node_mock.reset_mock()
# response.iter_content.return_value = BytesIO(file_content)
# headers.get.return_value = None
# session.put.reset_mock()
# session.put.side_effect = \
# [exceptions.TransferException()] * 2 * vos.MAX_INTERMTTENT_RETRIES
# with pytest.raises(OSError):
# test_client.copy(osLocation, vospaceLocation, head=True)
# assert session.put.call_count == 2 * vos.MAX_INTERMTTENT_RETRIES
#
# # test GET Transfer error on one URL and a "permanent" one on the other
# props.get.side_effect = md5sum
# get_node_url_mock = Mock(
# return_value=['http://cadc1.ca/test', 'http://cadc2.ca/test'])
# test_client.get_node_url = get_node_url_mock
# get_node_mock.reset_mock()
# response.iter_content.return_value = BytesIO(file_content)
# headers.get.return_value = None
# session.put.reset_mock()
# session.put.side_effect = [exceptions.TransferException(),
# exceptions.HttpException(),
# exceptions.TransferException(),
# exceptions.TransferException()]
# with pytest.raises(OSError):
# test_client.copy(osLocation, vospaceLocation, head=True)
# assert session.put.call_count == vos.MAX_INTERMTTENT_RETRIES + 1
#
# # test GET both "permanent" errors
# props.get.side_effect = md5sum
# get_node_url_mock = Mock(
# return_value=['http://cadc1.ca/test', 'http://cadc2.ca/test'])
# test_client.get_node_url = get_node_url_mock
# get_node_mock.reset_mock()
# response.iter_content.return_value = BytesIO(file_content)
# headers.get.return_value = None
# session.put.reset_mock()
# session.put.side_effect = [exceptions.HttpException(),
# exceptions.HttpException()]
# with pytest.raises(OSError):
# test_client.copy(osLocation, vospaceLocation, head=True)
# assert session.put.call_count == 2

def test_add_props(self):
old_node = Node(ElementTree.fromstring(NODE_XML))
Expand Down
Loading

0 comments on commit 9b115cb

Please sign in to comment.