Skip to content

Commit 280aa5c

Browse files
walker-jCharlesworth
authored andcommitted
Eva 'run' & 'go to' methods now use the correct renew period for the passed mode.
* Allow nested locker contexts to temporarily set new renew period * Added more locker tests
1 parent 73b1c47 commit 280aa5c

File tree

3 files changed

+259
-39
lines changed

3 files changed

+259
-39
lines changed

evasdk/Eva.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,20 @@ def control_home(self, wait_for_ready=True):
168168

169169
def control_run(self, loop=1, wait_for_ready=True, mode='teach'):
170170
self.__logger.debug('Eva.control_run called')
171-
return self.__http_client.control_run(loop=loop, wait_for_ready=wait_for_ready, mode=mode)
171+
if mode == 'teach':
172+
with self.__eva_locker.set_renew_period(3):
173+
return self.__http_client.control_run(loop=loop, wait_for_ready=wait_for_ready, mode=mode)
174+
else:
175+
return self.__http_client.control_run(loop=loop, wait_for_ready=wait_for_ready, mode=mode)
172176

173177

174178
def control_go_to(self, joints, wait_for_ready=True, velocity=None, duration=None, mode='teach'):
175-
self.__logger.debug('Eva.control_go_to called')
176-
return self.__http_client.control_go_to(joints, wait_for_ready=wait_for_ready, velocity=velocity, duration=duration, mode=mode)
179+
self.__logger.info('Eva.control_go_to called')
180+
if mode == 'teach':
181+
with self.__eva_locker.set_renew_period(3):
182+
return self.__http_client.control_go_to(joints, wait_for_ready=wait_for_ready, velocity=velocity, duration=duration, mode=mode)
183+
else:
184+
return self.__http_client.control_go_to(joints, wait_for_ready=wait_for_ready, velocity=velocity, duration=duration, mode=mode)
177185

178186

179187
def control_pause(self, wait_for_paused=True):

evasdk/eva_locker.py

Lines changed: 72 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,95 @@
22

33
from .eva_errors import EvaLockError
44

5+
# TODO improve timing accuracy by taking into account lock_renew() API call duration in the __renewal_timer() loop
6+
# TODO properly handle exceptions thrown in the renewal thread
7+
# TODO properly handle unlock on program exit
58

69
class EvaWithLocker:
710
"""
8-
The EvaWithLocker class is used to keep an Eva locked for the entirety of a Python with scope.
11+
The EvaWithLocker class is used to keep an Eva locked for the entirety of a Python 'with' scope.
912
It expects an already locked Eva object to be passed in, and for the duration of the
10-
with scope it will renew the lock every <renew_period> seconds. At the end of the scope
13+
with scope it will renew the lock every <renew_period> seconds.
14+
15+
'with' scopes can be nested, with the lock being renewed in a particular
16+
scope for the currently set 'renew_period' of the locker. At the end of the outer-most scope
1117
it will release the lock.
1218
"""
13-
def __init__(self, eva, renew_period=30):
19+
20+
21+
def __init__(self, eva, fallback_renew_period=30):
1422
self.__eva = eva
15-
self.__renew_period = renew_period
16-
self.__locked = False
23+
self.__fallback_renew_period = fallback_renew_period
24+
self.__renew_period = fallback_renew_period
25+
self.__period_stack = []
1726
self.__thread = None
1827
self.__cond = None
1928

2029

30+
def set_renew_period(self, renew_period=None):
31+
if renew_period == None:
32+
self.__renew_period = self.__fallback_renew_period
33+
else:
34+
self.__renew_period = renew_period
35+
return self
36+
37+
2138
def __enter__(self):
22-
if self.__locked:
23-
raise EvaLockError('Eva already locked in another "with" statement scope')
39+
if len(self.__period_stack) == 0:
40+
self.__try_renew()
2441

25-
try:
26-
self.__eva.lock_renew()
27-
except Exception:
28-
raise EvaLockError('"with eva:" statement requires a locked eva object, try "with eva.lock():"')
42+
self.__cond = threading.Condition()
43+
self.__thread = threading.Thread(target=self.__renewal_timer)
2944

30-
self.__locked = True
31-
self.__cond = threading.Condition()
32-
self.__thread = threading.Thread(target=self.__start_loop)
33-
self.__thread.start()
45+
self.__period_stack.append(self.__renew_period)
46+
self.__thread.start()
47+
else:
48+
with self.__cond:
49+
if self.__renew_period != self.__period_stack[-1]:
50+
self.__try_renew()
51+
52+
self.__period_stack.append(self.__renew_period)
53+
self.__reset_timer()
54+
else:
55+
raise EvaLockError("""Unneccesary refresh of the lock renewal process,
56+
lock is already being renewed with the configured period.""")
3457

3558

3659
def __exit__(self, type, value, traceback):
60+
context_end_size = None
61+
3762
with self.__cond:
38-
self.__locked = False
39-
self.__cond.notify_all()
40-
self.__thread.join()
63+
self.__period_stack.pop()
64+
self.__reset_timer()
4165

66+
context_end_size = len(self.__period_stack)
67+
if context_end_size != 0:
68+
self.__try_renew()
4269

43-
def __start_loop(self):
44-
while True:
45-
with self.__cond:
46-
self.__cond.wait(timeout=self.__renew_period)
47-
if self.__locked:
48-
self.__eva.lock_renew()
49-
else:
50-
self.__eva.unlock()
51-
return
70+
if context_end_size == 0:
71+
self.__thread.join()
72+
self.set_renew_period()
73+
74+
75+
def __renewal_timer(self):
76+
with self.__cond:
77+
while True:
78+
if not self.__cond.wait(timeout=self.__period_stack[-1]):
79+
# timeout has occurred: renewing
80+
self.__eva.lock_renew()
81+
82+
if len(self.__period_stack) == 0:
83+
self.__eva.unlock()
84+
return
85+
86+
87+
def __try_renew(self):
88+
try:
89+
self.__eva.lock_renew()
90+
except Exception:
91+
raise EvaLockError("""'with eva:' context statements require a locked eva object, e.g. by using 'with eva.lock():',
92+
and not unlocking from within the statement.""")
93+
94+
95+
def __reset_timer(self):
96+
self.__cond.notify()

tests/eva_locker_test.py

Lines changed: 176 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,41 +35,208 @@ def test_lock_no_renew(self):
3535
self.fail("should not raise an exception")
3636

3737
self.mock.lock.assert_not_called()
38-
self.mock.unlock.assert_called_once()
3938
self.mock.lock_renew.assert_called_once()
39+
self.mock.unlock.assert_called_once()
4040

4141

4242
def test_lock_with_renew(self):
4343
try:
4444
with self.testEvaWithLocker:
45-
time.sleep(0.02)
45+
time.sleep(0.015)
4646
except Exception:
4747
self.fail("should not raise an exception")
4848

4949
self.mock.lock.assert_not_called()
50+
self.assertEqual(self.mock.lock_renew.call_count, 2)
5051
self.mock.unlock.assert_called_once()
51-
assert self.mock.lock_renew.call_count == 2
5252

5353

54-
def test_double_lock_should_raise(self):
54+
def test_not_locked_should_raise(self):
55+
self.mock.lock_renew.side_effect = Exception("can't renew an unlocked eva")
56+
57+
with self.assertRaises(Exception):
58+
with self.testEvaWithLocker:
59+
self.fail("did not raise exception when renewing a not locked Eva")
60+
61+
self.mock.lock.assert_not_called()
62+
self.mock.lock_renew.assert_called_once()
63+
self.mock.unlock.assert_not_called()
64+
65+
66+
def test_nested_locker_same_period_should_raise(self):
5567
with self.assertRaises(Exception):
5668
with self.testEvaWithLocker:
5769
with self.testEvaWithLocker:
5870
self.fail("did not raise exception when locking an already locked Eva")
71+
5972
self.mock.lock.assert_not_called()
60-
self.mock.unlock.assert_called_once()
6173
self.mock.lock_renew.assert_called_once()
74+
self.mock.unlock.assert_called_once()
6275

6376

64-
def test_not_locked_should_raise(self):
65-
self.mock.lock_renew.side_effect = Exception("can't renew a unlocked eva")
77+
def test_nested_locker_entry_and_exit_renews_lock(self):
78+
try:
79+
with self.testEvaWithLocker:
80+
with self.testEvaWithLocker.set_renew_period(1):
81+
pass
82+
except Exception:
83+
self.fail("should not raise an exception")
84+
85+
self.mock.lock.assert_not_called()
86+
self.assertEqual(self.mock.lock_renew.call_count, 3)
87+
self.mock.unlock.assert_called_once()
88+
89+
90+
def test_nested_locker_different_period_with_inner_renew(self):
91+
try:
92+
with self.testEvaWithLocker:
93+
time.sleep(0.005)
94+
with self.testEvaWithLocker.set_renew_period(0.05):
95+
time.sleep(0.09)
96+
except Exception:
97+
self.fail("should not raise an exception")
98+
99+
self.mock.lock.assert_not_called()
100+
self.assertEqual(self.mock.lock_renew.call_count, 4)
101+
self.mock.unlock.assert_called_once()
102+
103+
104+
def test_nested_locker_different_period_with_outer_renew(self):
105+
try:
106+
with self.testEvaWithLocker:
107+
with self.testEvaWithLocker.set_renew_period(0.1):
108+
time.sleep(0.01)
109+
time.sleep(0.015)
110+
except Exception:
111+
self.fail("should not raise an exception")
112+
113+
self.mock.lock.assert_not_called()
114+
self.assertEqual(self.mock.lock_renew.call_count, 4)
115+
self.mock.unlock.assert_called_once()
116+
117+
118+
def test_nested_locker_reset_on_outer_exit(self):
119+
try:
120+
# 3 renews from this context
121+
with self.testEvaWithLocker:
122+
with self.testEvaWithLocker.set_renew_period(0.1):
123+
pass
124+
125+
with self.testEvaWithLocker:
126+
# Sleep duration < 0.1s should still cause a renew
127+
time.sleep(0.015)
128+
except Exception:
129+
self.fail("should not raise an exception")
130+
131+
self.mock.lock.assert_not_called()
132+
self.assertEqual(self.mock.lock_renew.call_count, 5)
133+
self.assertEqual(self.mock.unlock.call_count, 2)
134+
135+
136+
def test_deeply_nested_contexts(self):
137+
try:
138+
with self.testEvaWithLocker:
139+
with self.testEvaWithLocker.set_renew_period(0.3):
140+
with self.testEvaWithLocker.set_renew_period(0.45):
141+
with self.testEvaWithLocker.set_renew_period(0.075):
142+
# Sleep duration should cause a renew here
143+
time.sleep(0.078)
144+
except Exception:
145+
self.fail("should not raise an exception")
146+
147+
self.mock.lock.assert_not_called()
148+
self.assertEqual(self.mock.lock_renew.call_count, 8)
149+
self.mock.unlock.assert_called_once()
150+
151+
152+
def test_locker_has_default_period(self):
153+
self.testEvaWithLocker = EvaWithLocker(self.mock)
154+
155+
try:
156+
with self.testEvaWithLocker:
157+
# To avoid long tests: Sleep duration shorter than
158+
# the relatively long default should not cause a renew here
159+
time.sleep(0.5)
160+
except Exception:
161+
self.fail("should not raise an exception")
162+
163+
self.mock.lock.assert_not_called()
164+
self.assertEqual(self.mock.lock_renew.call_count, 1)
165+
self.mock.unlock.assert_called_once()
166+
167+
168+
def test_can_set_period_to_default(self):
169+
try:
170+
with self.testEvaWithLocker:
171+
with self.testEvaWithLocker.set_renew_period(1):
172+
with self.testEvaWithLocker.set_renew_period():
173+
# Sleep duration should cause a renew here
174+
time.sleep(0.011)
175+
except Exception:
176+
self.fail("should not raise an exception")
177+
178+
self.mock.lock.assert_not_called()
179+
self.assertEqual(self.mock.lock_renew.call_count, 6)
180+
self.mock.unlock.assert_called_once()
181+
182+
183+
def test_nested_locker_uses_last_set_period(self):
184+
try:
185+
with self.testEvaWithLocker:
186+
self.testEvaWithLocker.set_renew_period(0.02)
187+
with self.testEvaWithLocker.set_renew_period(0.1):
188+
# Sleep duration should not cause a renew here
189+
time.sleep(0.05)
190+
except Exception:
191+
self.fail("should not raise an exception")
192+
193+
self.mock.lock.assert_not_called()
194+
self.assertEqual(self.mock.lock_renew.call_count, 3)
195+
self.mock.unlock.assert_called_once()
196+
197+
198+
def test_nested_locker_returns_to_parent_period(self):
199+
try:
200+
with self.testEvaWithLocker:
201+
# Each sleep duration should cause a single renew here
202+
with self.testEvaWithLocker.set_renew_period(0.1):
203+
time.sleep(0.11)
204+
time.sleep(0.011)
205+
except Exception:
206+
self.fail("should not raise an exception")
207+
208+
self.mock.lock.assert_not_called()
209+
self.assertEqual(self.mock.lock_renew.call_count, 5)
210+
self.mock.unlock.assert_called_once()
211+
212+
213+
def test_handles_nested_renew_failure_on_enter(self):
214+
self.mock.lock_renew.side_effect = [None, Exception("Lock could not be renewed")]
215+
216+
with self.assertRaises(Exception):
217+
with self.testEvaWithLocker:
218+
with self.testEvaWithLocker.set_renew_period(0.1):
219+
self.fail("did not raise exception when renewing a not locked Eva")
220+
221+
self.mock.lock.assert_not_called()
222+
self.assertEqual(self.mock.lock_renew.call_count, 2)
223+
# Should have final unlock
224+
self.mock.unlock.assert_called_once()
225+
226+
227+
def test_handles_nested_renew_failure_on_exit(self):
228+
self.mock.lock_renew.side_effect = [None, None, Exception("Lock could not be renewed")]
66229

67230
with self.assertRaises(Exception):
68231
with self.testEvaWithLocker:
232+
with self.testEvaWithLocker.set_renew_period(0.1):
233+
pass
69234
self.fail("did not raise exception when renewing a not locked Eva")
235+
70236
self.mock.lock.assert_not_called()
71-
self.mock.unlock.assert_not_called()
72-
self.mock.lock_renew.assert_called_once()
237+
self.assertEqual(self.mock.lock_renew.call_count, 3)
238+
# Should have final unlock
239+
self.mock.unlock.assert_called_once()
73240

74241

75242
if __name__ == '__main__':

0 commit comments

Comments
 (0)