theine icon indicating copy to clipboard operation
theine copied to clipboard

Future exception was never retrieved problem when use with decorator in v2.0

Open shadowlinyf opened this issue 2 months ago • 1 comments

how to reproduce the problem:

@Memoize(10000, timedelta(seconds=100))
async def foo_a(a:int) -> int:
    raise Exception("test")
    return a

@foo_a.key
def _(a:int) -> str:
    return f"a:{a}"

async def main():
    await foo_a(1)

if __name__ == "__main__":
    asyncio.run(main())

result:

Future exception was never retrieved
future: <Future finished exception=Exception('test')>
Traceback (most recent call last):
  File "\lib\site-packages\theine\theine.py", line 79, in __await__
    result = yield from self.awaitable.__await__()
  File "\test\theine_demo.py", line 20, in foo_a
    raise Exception("test")
Exception: test
Traceback (most recent call last):
  File "\test\theine_demo.py", line 38, in <module>
    asyncio.run(main())
  File "\lib\asyncio\runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "\lib\asyncio\base_events.py", line 649, in run_until_complete
    return future.result()
  File "\test\theine_demo.py", line 29, in main
    await foo_a(1)
  File "\lib\site-packages\theine\theine.py", line 90, in __await__
    raise e
  File "\lib\site-packages\theine\theine.py", line 79, in __await__
    result = yield from self.awaitable.__await__()
  File "\testt\theine_demo.py", line 20, in foo_a
    raise Exception("test")
Exception: test

shadowlinyf avatar Oct 28 '25 06:10 shadowlinyf

It seem if you change the CachedAwaitable class to this could solve the problem. but I am no expert with python async I am not sure if this could cause any side effect.

                if self.future:
                    self.future.set_exception(e)
                    # add this to consume the exception?
                    self.future.exception()
# https://github.com/python/cpython/issues/90780
# use event to protect from thundering herd
class CachedAwaitable:
    def __init__(
        self, awaitable: Awaitable[Any], on_error: Callable[[BaseException], None]
    ) -> None:
        self.awaitable = awaitable
        self.future: Optional[Awaitable[Any]] = None
        self.result = sentinel
        self.on_error = on_error
        self.exception: Optional[BaseException] = None

    def __await__(self) -> Any:
        if self.result is not sentinel:
            return self.result
        elif self.exception:
            raise self.exception

        if self.future is None:
            try:
                self.future = asyncio.Future()
                result = yield from self.awaitable.__await__()
                self.result = result
                self.future.set_result(self.result)
                self.future = None
                return result
            except BaseException as e:
                self.exception = e
                if self.future:
                    self.future.set_exception(e)
                    # add this to consume the exception?
                    self.future.exception()
                self.future = None
                self.on_error(e)
                raise e
        else:
            yield from self.future.__await__()
        return self.result

shadowlinyf avatar Oct 28 '25 06:10 shadowlinyf