1

I have this pytest-powered test function that seems functional :

def test_settrace_timeout():
    TIMEOUT_DURATION = 3

    @settrace_timeout(TIMEOUT_DURATION)
    def function_to_timeout(n):
        sleep(n)

    with pytest.raises(TimeoutError):
        function_to_timeout(TIMEOUT_DURATION + 1)
    function_to_timeout(TIMEOUT_DURATION - 1)

It's meant to test whether the parametrized settrace_timeout decorator makes decorated functions raise a TimeoutError when it takes too long and doesn't make it raise it when it return soon enough. In that test, I'm decorating a function that uses time.sleep.

It looks functional as a test. The tested decorator works and fails as expected and "sabotaging" it makes the test fail.

So I also tried this out of curiosity (and the test still works) :

As far as I know it should be nearly equivalent.

def test_settrace_timeout():
    TIMEOUT_DURATION = 3

    function_to_timeout = settrace_timeout(TIMEOUT_DURATION)(lambda n: sleep(n))

    with pytest.raises(TimeoutError):
        function_to_timeout(TIMEOUT_DURATION + 1)
    function_to_timeout(TIMEOUT_DURATION - 1)

And finally, I have this that should work (that's what I was expecting at least). It doesn't work though.

When I try to remove the lambda proxy function, and when I try to directly decorate the sleep function, it suddenly fails (doesn't raise the function when expected to).

def test_settrace_timeout():
    TIMEOUT_DURATION = 3

    function_to_timeout = settrace_timeout(TIMEOUT_DURATION)(sleep)  #  Syntax it suddenly fails with (no more raised exception).

    with pytest.raises(TimeoutError):
        function_to_timeout(TIMEOUT_DURATION + 1)
    function_to_timeout(TIMEOUT_DURATION - 1)

What is the reason why I have this different behaviour in this specific case? I'd like to understand the difference between the test functions.


Here is the decorator I'm trying to test :

def settrace_timeout(timeout_in_seconds):
    """
    sys.settrace() based timeout.
    Will raise TimeoutError.
    It's meant to be used with requests based methods.

    It's based on this SO answer :
    https://stackoverflow.com/a/71453648/3156085
    """

    def decorator(f):

        exception_class = TimeoutError
        f_name = f.__name__

        def function(*args, **kwargs):
            start = time.time()

            def trace_function(frame, event, arg):
                if time.time() - start > timeout_in_seconds:
                    log("[settrace_timeout] - Timeout occured in function {0}.".format(f_name))
                    raise exception_class("Timeout")
                else:
                    return trace_function

            try:
                sys.settrace(trace_function)
                log("[settrace_timeout] settrace enabled in function {0}.".format(f_name))
                return_value = f(*args, **kwargs)
            except exception_class as e:
                log("[settrace_timeout] exception {0} raised in function {1}.".format(e, f_name))
                raise
            finally:
                sys.settrace(None)
                log("[settrace_timeout] settrace disabled in function {0}.".format(f_name))
            return return_value

        return function

    return decorator

vmonteco
  • 14,136
  • 15
  • 55
  • 86
  • 1
    This is just a guess: settrace is a debugging feature. Maybe the interpreter does not call the settrace function for built-in functions. That would not make any sense as there is no python code to debug. So it skips all those functions that are implemented in C. BTW: your function does not seem to work at all. It raises a TimeoutError **after** the function has completed. – Wombatz Jul 26 '22 at 13:01
  • @Wombatz It looks like you're right about settrace not tracing C based builtins (https://stackoverflow.com/a/16119785/3156085). About my function, do you mean that once the decorated function ends, the ongoing settrace will still eventually end up in raising a `TimeoutError`? Are you sure about that? That's not what I observe. – vmonteco Jul 26 '22 at 13:21
  • You've got some nice log messages in `settrace_timeout()`. Can you update the Q with confirmation from the logs on which branches are taken in the working and non-working cases? – Sarah Messer Jul 26 '22 at 13:43
  • @vmonteco No, take this example: `settrace_timeout(1)(lambda x: sleep(10))()` It will run for 10 seconds and **then** it will raise a TimeoutError. It cannot interrupt the sleep function. – Wombatz Jul 26 '22 at 13:57

0 Answers0