func_timeout icon indicating copy to clipboard operation
func_timeout copied to clipboard

Cannot use it in a nested manner

Open zachliu opened this issue 4 years ago • 2 comments
trafficstars

I'm using it under Python 3.8.4

from time import sleep

from func_timeout import func_set_timeout


@func_set_timeout(1)
def func_1(sec):
    print(f"in func 1, sleeping {sec} sec but will timeout in 1 sec")
    sleep(sec)


@func_set_timeout(1)
def func_2(sec):
    print(f"in func 2, sleeping {sec} sec but will timeout in 1 sec")
    sleep(sec)


@func_set_timeout(5)
def func():
    func_1(2)
    print("in main func")
    func_2(0.3)


if __name__ == "__main__":
    func()

This program would stop at func_1(2) silently it seems the top level @func_set_timeout(5) swallows all the FunctionTimedOut and doesn't re-raise

zachliu avatar Aug 17 '21 01:08 zachliu

i think this is why but i don't understand the reason behind it https://github.com/kata198/func_timeout/blob/50baa8db502fd24acc0f2a1bc473649505336997/func_timeout/dafunc.py#L69-L71

in order to use it in a nested manner, we need to capture the FunctionTimedOut and re-raise as something else such as TimeoutError for it to be captured by the top-level @func_set_timeout(5)

from functools import wraps
from time import sleep
from typing import Any, Callable, Type

from func_timeout import FunctionTimedOut, func_set_timeout


def retry(
    *exceptions: Type[Exception], count: int = 20, wait_time: int = 20,
) -> Callable:
    """Enable retry option for a set of exceptions
    """

    def retry_decorator(func: Callable) -> Callable:
        """Decorator"""

        @wraps(func)
        def func_wrapper(
            *args: Any, **kwargs: Any
        ) -> Any:
            """Function wrapper"""
            for i in range(count):
                try:
                    return func(*args, **kwargs)
                except exceptions as exception:  # pragma: no cover
                    print(
                        f"Retrying {i+1}: exception='{exception}', "
                        f"total {count} retries, wait {wait_time} seconds"
                    )
                    sleep(wait_time)

                    if i == count - 1:
                        if isinstance(exception, FunctionTimedOut):
                            raise TimeoutError("Re-raise as TimeoutError")
                        raise exception

        return func_wrapper

    return retry_decorator


@retry(
    FunctionTimedOut, count=2, wait_time=1,
)
@func_set_timeout(1)
def func_1(sec):
    print(f"in func 1, sleeping {sec} sec but will timeout in 1 sec")
    sleep(sec)


@retry(
    FunctionTimedOut, count=2, wait_time=1,
)
@func_set_timeout(1)
def func_2(sec):
    print(f"in func 2, sleeping {sec} sec but will timeout in 1 sec")
    sleep(sec)


@retry(
    FunctionTimedOut, count=2, wait_time=1,
)
@func_set_timeout(5)
def func():
    func_1(2)
    print("in main func")
    func_2(0.3)


if __name__ == "__main__":
    func()

zachliu avatar Aug 17 '21 14:08 zachliu

Sorry for the delay, I've been away.

I've fixed this issue. The change is committed under the currently unreleased 4.4branch (I need to write tests and such)

If you want technical details on why that "pass" statement is there, it's to avoid the default exception handler which we cannot control, and it spams stderr. You used to be able to override it, but not on anything modern. You can see the spam if you change that "pass" to "raise" and run the test suite.

I've updated the code so that we now explicitly check if we are going to raise the exception into another StoppableThread, in which case 4.4branch now does. Otherwise, (returning to the MainThread or a different type of thread) we continue to eat the error.

The test suite passes with this change, and your testcase also passes. I'll write some test cases to make sure this covers the issue completely, and it stays covered.

Thanks for the report!

kata198 avatar Apr 23 '23 08:04 kata198