Skip to content

Commit

Permalink
Merge pull request #7783 from drew2a/fix/6915
Browse files Browse the repository at this point in the history
Fix RuntimeError: invalid torrent handle used
  • Loading branch information
drew2a committed Dec 27, 2023
2 parents f17ed57 + dd23d70 commit 5b34c58
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 7 deletions.
118 changes: 118 additions & 0 deletions src/tribler/core/components/libtorrent/tests/test_torrent_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from asyncio import Future
from unittest.mock import Mock

import pytest

from tribler.core.components.libtorrent.utils.torrent_utils import require_handle


def create_download(handle=None, handle_future_cancelled=False):
""" Create a download object with a handle.
Args:
handle: The handle to use for the download
handle_future_cancelled: Whether the future for the handle should be cancelled
Returns: A download object with a handle
"""
handle = handle or Mock()
future_for_handle = Future()
if not handle_future_cancelled:
future_for_handle.set_result(handle)
else:
future_for_handle.cancel()

download = Mock(get_handle=Mock(return_value=future_for_handle))
download.handle = handle
return download


async def test_require_handle_not_valid():
# Test that the function is not invoked when the handle is not valid

@require_handle
def f(_):
return "result"

handle = Mock(is_valid=Mock(return_value=False))
download = create_download(handle)

result = await f(download)

assert result is None


async def test_require_handles_not_equal():
# Test that the function is not invoked when the handles is not equal

@require_handle
def f(_):
return "result"

download = create_download()
download.handle = Mock() # Change the handle to a different object to provoke the not equal check

result = await f(download)

assert result is None


async def test_require_handle_future_cancelled():
# Test that the function is not invoked when the handle future is cancelled

@require_handle
def f(_):
return "result"

download = create_download(handle_future_cancelled=True)

result = await f(download)

assert result is None


async def test_require_handle_result_future_done():
# Test that the function is not invoked when the result future is done

@require_handle
def f(_):
return "result"

download = create_download()

future = f(download)
future.set_result('any result') # Set the result future to done to provoke the result_future.done() check
result = await future

# The result should not be the result of the function which is indicator that the function was not invoked
assert result != "result"


async def test_require_handle_result_exception():
# Test that the require_handle re-raises an exception when the function raises an exception

class TestException(Exception):
""" Exception for testing """

@require_handle
def f(_):
raise TestException

download = create_download()

with pytest.raises(TestException):
await f(download)


async def test_require_handle_result_runtime_error():
# RuntimeError is treated specially in the require_handle decorator exceptions of this type are logged
# but not re-raised
@require_handle
def f(_):
raise RuntimeError

download = create_download()

result = await f(download)

assert result is None
24 changes: 17 additions & 7 deletions src/tribler/core/components/libtorrent/utils/torrent_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from hashlib import sha1
from typing import Any, Dict, Iterable, List, Optional

from tribler.core.components.libtorrent import torrentdef
from tribler.core.components.libtorrent.utils.libtorrent_helper import libtorrent as lt
from tribler.core.utilities.path_util import Path

Expand Down Expand Up @@ -34,19 +33,30 @@ def require_handle(func):
Invoke the function once the handle is available. Returns a future that will fire once the function has completed.
Author(s): Egbert Bouman
"""

def invoke_func(*args, **kwargs):
result_future = Future()

def done_cb(fut):
with suppress(CancelledError):
handle = fut.result()

if not fut.cancelled() \
and not result_future.done() \
and handle == download.handle \
and handle.is_valid() \
and not isinstance(download.tdef, torrentdef.TorrentDefNoMetainfo):
result_future.set_result(func(*args, **kwargs))
if fut.cancelled() or result_future.done() or handle != download.handle or not handle.is_valid():
logger.warning('Can not invoke function, handle is not valid or future is cancelled')
result_future.set_result(None)
return

try:
result = func(*args, **kwargs)
except RuntimeError as e:
# ignore runtime errors, for more info see: https://github.com/Tribler/tribler/pull/7783
logger.exception(e)
result_future.set_result(None)
except Exception as e:
logger.exception(e)
result_future.set_exception(e)
else:
result_future.set_result(result)

download = args[0]
handle_future = download.get_handle()
Expand Down

0 comments on commit 5b34c58

Please sign in to comment.