Skip to content

Commit 1e2a8a1

Browse files
authored
Merge pull request #1234 from openstack-charmers/test-cinder-client-retrier
Add ObjectRetrier to CinderaBackupTests
2 parents ce1ec55 + 5b6049b commit 1e2a8a1

File tree

6 files changed

+81
-25
lines changed

6 files changed

+81
-25
lines changed

requirements.txt

+4-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ python-novaclient
5050
python-octaviaclient
5151
python-swiftclient
5252
python-watcherclient
53-
tenacity
53+
# Due to https://github.com/jd/tenacity/pull/479 the strategy for mocking out tenacity
54+
# waits/times/etc no longer works. Pin to 8.4.1 until it is solved.
55+
# Bug in tenacity tracking issue: https://github.com/jd/tenacity/issues/482
56+
tenacity<8.4.2
5457
paramiko
5558

5659
# Documentation requirements

unit_tests/utilities/test_utilities.py

+23-3
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,28 @@ def func(self):
113113

114114
mock_sleep.assert_not_called()
115115

116+
@mock.patch("time.sleep")
117+
def test_object_wrap_multilevel_with_exception(self, mock_sleep):
118+
119+
class A:
120+
121+
def func(self):
122+
raise SomeException()
123+
124+
class B:
125+
126+
def __init__(self):
127+
self.a = A()
128+
129+
b = B()
130+
# retry on a specific exception
131+
wrapped_b = utilities.ObjectRetrierWraps(
132+
b, num_retries=1, retry_exceptions=[SomeException])
133+
with self.assertRaises(SomeException):
134+
wrapped_b.a.func()
135+
136+
mock_sleep.assert_called_once_with(5)
137+
116138
@mock.patch("time.sleep")
117139
def test_log_called(self, mock_sleep):
118140

@@ -128,9 +150,7 @@ def func(self):
128150
with self.assertRaises(SomeException):
129151
wrapped_a.func()
130152

131-
# there should be two calls; one for the single retry and one for the
132-
# failure.
133-
self.assertEqual(mock_log.call_count, 2)
153+
mock_log.assert_called()
134154

135155
@mock.patch("time.sleep")
136156
def test_back_off_maximum(self, mock_sleep):

unit_tests/utilities/test_zaza_utilities_openstack.py

+3
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ def setUp(self):
9999
self.neutronclient.list_agents.return_value = self.agents
100100
self.neutronclient.list_bgp_speaker_on_dragent.return_value = \
101101
self.bgp_speakers
102+
self.patch("zaza.openstack.utilities.ObjectRetrierWraps",
103+
name="_object_retrier_wraps",
104+
new=lambda x, *_, **__: x)
102105

103106
def test_create_port(self):
104107
self.patch_object(openstack_utils, "get_net_uuid")

zaza/openstack/charm_tests/cinder_backup/tests.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import zaza.model
2424
import zaza.openstack.charm_tests.test_utils as test_utils
25+
from zaza.openstack.utilities import retry_on_connect_failure
2526
import zaza.openstack.utilities.ceph as ceph_utils
2627
import zaza.openstack.utilities.openstack as openstack_utils
2728

@@ -35,8 +36,9 @@ class CinderBackupTest(test_utils.OpenStackBaseTest):
3536
def setUpClass(cls):
3637
"""Run class setup for running Cinder Backup tests."""
3738
super(CinderBackupTest, cls).setUpClass()
38-
cls.cinder_client = openstack_utils.get_cinder_session_client(
39-
cls.keystone_session)
39+
cls.cinder_client = retry_on_connect_failure(
40+
openstack_utils.get_cinder_session_client(cls.keystone_session),
41+
log=logging.warn)
4042

4143
@property
4244
def services(self):
@@ -101,7 +103,7 @@ def test_410_cinder_vol_create_backup_delete_restore_pool_inspect(self):
101103
self.cinder_client.volumes,
102104
cinder_vol.id,
103105
wait_iteration_max_time=180,
104-
stop_after_attempt=15,
106+
stop_after_attempt=30,
105107
expected_status='available',
106108
msg='ceph-backed cinder volume')
107109

@@ -123,7 +125,7 @@ def test_410_cinder_vol_create_backup_delete_restore_pool_inspect(self):
123125
self.cinder_client.backups,
124126
vol_backup.id,
125127
wait_iteration_max_time=180,
126-
stop_after_attempt=15,
128+
stop_after_attempt=30,
127129
expected_status='available',
128130
msg='Backup volume')
129131

zaza/openstack/charm_tests/manila/tests.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ def test_manila_share(self):
359359
self.manila_client.shares,
360360
share.id,
361361
wait_iteration_max_time=120,
362-
stop_after_attempt=2,
362+
stop_after_attempt=10,
363363
expected_status="available",
364364
msg="Waiting for a share to become available")
365365

zaza/openstack/utilities/__init__.py

+44-16
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,30 @@
1515
"""Collection of utilities to support zaza tests etc."""
1616

1717

18+
import logging
1819
import time
1920

2021
from keystoneauth1.exceptions.connection import ConnectFailure
2122

23+
NEVER_RETRY_EXCEPTIONS = (
24+
AssertionError,
25+
AttributeError,
26+
ImportError,
27+
IndexError,
28+
KeyError,
29+
NotImplementedError,
30+
OverflowError,
31+
RecursionError,
32+
ReferenceError,
33+
RuntimeError,
34+
SyntaxError,
35+
IndentationError,
36+
SystemExit,
37+
TypeError,
38+
UnicodeError,
39+
ZeroDivisionError,
40+
)
41+
2242

2343
class ObjectRetrierWraps(object):
2444
"""An automatic retrier for an object.
@@ -76,35 +96,39 @@ def __init__(self, obj, num_retries=3, initial_interval=5.0, backoff=1.0,
7696
If a list, then it will only retry if the exception is one of the
7797
ones in the list.
7898
:type retry_exceptions: List[Exception]
99+
:param log: If False, disable logging; if None (the default) or True,
100+
use logging.warn; otherwise use the passed param `log`.
101+
:type param: None | Boolean | Callable
79102
"""
80103
# Note we use semi-private variable names that shouldn't clash with any
81104
# on the actual object.
82105
self.__obj = obj
106+
if log in (None, True):
107+
_log = logging.warning
108+
elif log is False:
109+
_log = lambda *_, **__: None # noqa
110+
else:
111+
_log = log
83112
self.__kwargs = {
84113
'num_retries': num_retries,
85114
'initial_interval': initial_interval,
86115
'backoff': backoff,
87116
'max_interval': max_interval,
88117
'total_wait': total_wait,
89118
'retry_exceptions': retry_exceptions,
90-
'log': log or (lambda x: None),
119+
'log': _log,
91120
}
121+
_log(f"ObjectRetrierWraps: wrapping {self.__obj}")
92122

93123
def __getattr__(self, name):
94124
"""Get attribute; delegates to wrapped object."""
95-
# Note the above may generate an attribute error; we expect this and
96-
# will fail with an attribute error.
97-
attr = getattr(self.__obj, name)
98-
if callable(attr) or hasattr(attr, "__getattr__"):
125+
obj = self.__obj
126+
attr = getattr(obj, name)
127+
if callable(attr):
99128
return ObjectRetrierWraps(attr, **self.__kwargs)
100-
else:
129+
if attr.__class__.__module__ == 'builtins':
101130
return attr
102-
# TODO(ajkavanagh): Note detecting a property is a bit trickier. we
103-
# can do isinstance(attr, property), but then the act of accessing it
104-
# is what calls it. i.e. it would fail at the getattr(self.__obj,
105-
# name) stage. The solution is to check first, and if it's a property,
106-
# then treat it like the retrier. However, I think this is too
107-
# complex for the first go, and to use manual retries in that instance.
131+
return ObjectRetrierWraps(attr, **self.__kwargs)
108132

109133
def __call__(self, *args, **kwargs):
110134
"""Call the object; delegates to the wrapped object."""
@@ -126,16 +150,20 @@ def __call__(self, *args, **kwargs):
126150
# is not in the list of retries, then raise an exception
127151
# immediately. This means that if retry_exceptions is None,
128152
# then the method is always retried.
153+
if isinstance(e, NEVER_RETRY_EXCEPTIONS):
154+
log("ObjectRetrierWraps: error {} is never caught; raising"
155+
.format(str(e)))
156+
raise
129157
if (retry_exceptions is not None and
130158
type(e) not in retry_exceptions):
131159
raise
132160
retry += 1
133161
if retry > num_retries:
134-
log("{}: exceeded number of retries, so erroring out"
135-
.format(str(obj)))
162+
log("ObjectRetrierWraps: exceeded number of retries, "
163+
"so erroring out")
136164
raise e
137-
log("{}: call failed: retrying in {} seconds"
138-
.format(str(obj), wait))
165+
log("ObjectRetrierWraps: call failed: retrying in {} "
166+
"seconds" .format(wait))
139167
time.sleep(wait)
140168
wait_so_far += wait
141169
if wait_so_far >= total_wait:

0 commit comments

Comments
 (0)