Skip to content

Commit 1a5eeaa

Browse files
committed
Fixed sporadic problems with timeouts on Win Py2.6.
Timeout tests started to fail sporadically on Windows with Python 2.6 after recent refactoring of keyword execution logic. Based on git bisect the "bad commit" was 08390eb. The actual problem turned out to be in the code related to asynchronously raising exception in the runner thread to signal timeout. In some cases the exception was raised so that it occurred during the code that was trying to cancel it. Most likely the old keyword execution code catched the resulting TimeoutError but that didn't anymore happen after the refactoring. Not sure why that problem only manifested itself with Python 2.6, though. The fix has two parts: - Check has timeout occurred inside try/finally and wait for it to be actually raised. This ensures it won't happen inside the finally block. - Add new `_finished` flag to signal execution has finished and timeout should not be raised. Also use locks to avoid race conditions when setting `_finished` and `_timeout_occurred`. Interestingly the problem seems to vanish with just the first fix. Looking at the code, the problem could occur also then, so it is safer to keep both parts.
1 parent 951fdf8 commit 1a5eeaa

File tree

1 file changed

+29
-18
lines changed

1 file changed

+29
-18
lines changed

src/robot/running/timeouts/windows.py

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,45 +14,55 @@
1414

1515
import ctypes
1616
import time
17-
from threading import Timer, current_thread
17+
from threading import current_thread, Lock, Timer
1818

1919
from robot.errors import TimeoutError
2020
from robot.utils import py2to3
2121

2222

2323
class Timeout(object):
2424

25-
def __init__(self, timeout, timeout_error):
25+
def __init__(self, timeout, error):
2626
self._runner_thread_id = current_thread().ident
27-
self._timeout_error = self._create_timeout_error_class(timeout_error)
27+
self._timeout_error = self._create_timeout_error_class(error)
2828
self._timer = Timer(timeout, self._raise_timeout_error)
2929
self._timeout_occurred = False
30+
self._finished = False
31+
self._lock = Lock()
3032

31-
def _create_timeout_error_class(self, timeout_error):
33+
def _create_timeout_error_class(self, error):
3234
return py2to3(type(TimeoutError.__name__, (TimeoutError,),
33-
{'__unicode__': lambda self: timeout_error}))
35+
{'__unicode__': lambda self: error}))
3436

3537
def execute(self, runnable):
36-
self._start_timer()
3738
try:
38-
return runnable()
39+
self._start_timer()
40+
result = runnable()
41+
self._cancel_timer()
42+
self._wait_for_raised_timeout()
43+
return result
3944
finally:
40-
self._stop_timer()
45+
if self._timeout_occurred:
46+
raise self._timeout_error()
4147

4248
def _start_timer(self):
4349
self._timer.start()
4450

45-
def _stop_timer(self):
46-
self._timer.cancel()
47-
# In case timeout has occurred but the exception has not yet been
48-
# thrown we need to do this to ensure that the exception
49-
# is not thrown in an unsafe location
51+
def _cancel_timer(self):
52+
with self._lock:
53+
self._finished = True
54+
self._timer.cancel()
55+
56+
def _wait_for_raised_timeout(self):
5057
if self._timeout_occurred:
51-
self._cancel_exception()
52-
raise self._timeout_error()
58+
while True:
59+
time.sleep(0)
5360

5461
def _raise_timeout_error(self):
55-
self._timeout_occurred = True
62+
with self._lock:
63+
if self._finished:
64+
return
65+
self._timeout_occurred = True
5666
return_code = self._try_to_raise_timeout_error_in_runner_thread()
5767
# return code tells how many threads have been influenced
5868
while return_code > 1: # if more than one then cancel and retry
@@ -62,8 +72,9 @@ def _raise_timeout_error(self):
6272

6373
def _try_to_raise_timeout_error_in_runner_thread(self):
6474
return ctypes.pythonapi.PyThreadState_SetAsyncExc(
65-
self._runner_thread_id,
75+
ctypes.c_long(self._runner_thread_id),
6676
ctypes.py_object(self._timeout_error))
6777

6878
def _cancel_exception(self):
69-
ctypes.pythonapi.PyThreadState_SetAsyncExc(self._runner_thread_id, None)
79+
ctypes.pythonapi.PyThreadState_SetAsyncExc(
80+
ctypes.c_long(self._runner_thread_id), None)

0 commit comments

Comments
 (0)