Automatically refresh and expire the torrent status cache.

Stop at ratio was not working when no clients were connected, because
it was using a cached version of the torrent status, and never calling
for a refresh. When a client connected, it called for the refresh and
started working properly.

Closes: https://dev.deluge-torrent.org/ticket/3497
Closes: https://github.com/deluge-torrent/deluge/pull/369
This commit is contained in:
Chase Sterling 2022-02-08 14:34:02 -05:00 committed by Calum Lind
parent 62a4052178
commit 8ff4683780
No known key found for this signature in database
GPG Key ID: 90597A687B836BA3
3 changed files with 54 additions and 20 deletions

View File

@ -16,6 +16,8 @@ Attributes:
import logging
import os
import socket
import time
from typing import Optional
from urllib.parse import urlparse
from twisted.internet.defer import Deferred, DeferredList
@ -234,7 +236,8 @@ class Torrent:
self.handle = handle
self.magnet = magnet
self.status = self.handle.status()
self._status: Optional['lt.torrent_status'] = None
self._status_last_update: float = 0.0
self.torrent_info = self.handle.torrent_file()
self.has_metadata = self.status.has_metadata
@ -267,7 +270,6 @@ class Torrent:
self.prev_status = {}
self.waiting_on_folder_rename = []
self.update_status(self.handle.status())
self._create_status_funcs()
self.set_options(self.options)
self.update_state()
@ -641,7 +643,7 @@ class Torrent:
def update_state(self):
"""Updates the state, based on libtorrent's torrent state"""
status = self.handle.status()
status = self.get_lt_status()
session_paused = component.get('Core').session.is_paused()
old_state = self.state
self.set_status_message()
@ -709,7 +711,7 @@ class Torrent:
restart_to_resume (bool, optional): Prevent resuming clearing the error, only restarting
session can resume.
"""
status = self.handle.status()
status = self.get_lt_status()
self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=False,
@ -1024,7 +1026,7 @@ class Torrent:
dict: a dictionary of the status keys and their values
"""
if update:
self.update_status(self.handle.status())
self.get_lt_status()
if all_keys:
keys = list(self.status_funcs)
@ -1054,13 +1056,35 @@ class Torrent:
return status_dict
def update_status(self, status):
def get_lt_status(self) -> 'lt.torrent_status':
"""Get the torrent status fresh, not from cache.
This should be used when a guaranteed fresh status is needed rather than
`torrent.handle.status()` because it will update the cache as well.
"""
self.status = self.handle.status()
return self.status
@property
def status(self) -> 'lt.torrent_status':
"""Cached copy of the libtorrent status for this torrent.
If it has not been updated within the last five seconds, it will be
automatically refreshed.
"""
if self._status_last_update < (time.time() - 5):
self.status = self.handle.status()
return self._status
@status.setter
def status(self, status: 'lt.torrent_status') -> None:
"""Updates the cached status.
Args:
status (libtorrent.torrent_status): a libtorrent torrent status
status: a libtorrent torrent status
"""
self.status = status
self._status = status
self._status_last_update = time.time()
def _create_status_funcs(self):
"""Creates the functions for getting torrent status"""

View File

@ -279,11 +279,6 @@ class TorrentManager(component.Component):
'Paused',
'Queued',
):
# If the global setting is set, but the per-torrent isn't...
# Just skip to the next torrent.
# This is so that a user can turn-off the stop at ratio option on a per-torrent basis
if not torrent.options['stop_at_ratio']:
continue
if (
torrent.get_ratio() >= torrent.options['stop_ratio']
and torrent.is_finished
@ -291,7 +286,7 @@ class TorrentManager(component.Component):
if torrent.options['remove_at_ratio']:
self.remove(torrent_id)
break
if not torrent.handle.status().paused:
if not torrent.status.paused:
torrent.pause()
def __getitem__(self, torrent_id):
@ -1359,10 +1354,8 @@ class TorrentManager(component.Component):
torrent.set_tracker_status('Announce OK')
# Check for peer information from the tracker, if none then send a scrape request.
if (
alert.handle.status().num_complete == -1
or alert.handle.status().num_incomplete == -1
):
torrent.get_lt_status()
if torrent.status.num_complete == -1 or torrent.status.num_incomplete == -1:
torrent.scrape_tracker()
def on_alert_tracker_announce(self, alert):
@ -1612,7 +1605,7 @@ class TorrentManager(component.Component):
except RuntimeError:
continue
if torrent_id in self.torrents:
self.torrents[torrent_id].update_status(t_status)
self.torrents[torrent_id].status = t_status
self.handle_torrents_status_callback(self.torrents_status_requests.pop())

View File

@ -3,7 +3,7 @@
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
import itertools
import os
import time
from base64 import b64encode
@ -356,3 +356,20 @@ class TestTorrent(BaseTestCase):
self.torrent = Torrent(handle, {})
assert not self.torrent.connect_peer('127.0.0.1', 'text')
assert self.torrent.connect_peer('127.0.0.1', '1234')
def test_status_cache(self):
atp = self.get_torrent_atp('test_torrent.file.torrent')
handle = self.session.add_torrent(atp)
mock_time = mock.Mock(return_value=time.time())
with mock.patch('time.time', mock_time):
torrent = Torrent(handle, {})
counter = itertools.count()
handle.status = mock.Mock(side_effect=counter.__next__)
first_status = torrent.get_lt_status()
assert first_status == 0, 'sanity check'
assert first_status == torrent.status, 'cached status should be used'
assert torrent.get_lt_status() == 1, 'status should update'
assert torrent.status == 1
# Advance time and verify cache expires and updates
mock_time.return_value += 10
assert torrent.status == 2