From 8ff4683780921111f26fe051e0274aac8afe8bf3 Mon Sep 17 00:00:00 2001 From: Chase Sterling Date: Tue, 8 Feb 2022 14:34:02 -0500 Subject: [PATCH] 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 --- deluge/core/torrent.py | 40 ++++++++++++++++++++++++++++------- deluge/core/torrentmanager.py | 15 ++++--------- deluge/tests/test_torrent.py | 19 ++++++++++++++++- 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/deluge/core/torrent.py b/deluge/core/torrent.py index 9a4af0b8a2..57ec26f37a 100644 --- a/deluge/core/torrent.py +++ b/deluge/core/torrent.py @@ -16,6 +16,8 @@ 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 @@ def __init__(self, handle, options, state=None, filename=None, magnet=None): 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 @@ def __init__(self, handle, options, state=None, filename=None, magnet=None): 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 @@ def merge_trackers(self, torrent_info): 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 @@ def force_error_state(self, message, restart_to_resume=True): 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 @@ def get_status(self, keys, diff=False, update=False, all_keys=False): 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 @@ def get_status(self, keys, diff=False, update=False, all_keys=False): 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""" diff --git a/deluge/core/torrentmanager.py b/deluge/core/torrentmanager.py index 753bc894cb..41e1966b04 100644 --- a/deluge/core/torrentmanager.py +++ b/deluge/core/torrentmanager.py @@ -279,11 +279,6 @@ def update(self): '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 @@ def update(self): 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 @@ def on_alert_tracker_reply(self, alert): 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 @@ def on_alert_state_update(self, alert): 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()) diff --git a/deluge/tests/test_torrent.py b/deluge/tests/test_torrent.py index 6c07531dd9..36adc0fde7 100644 --- a/deluge/tests/test_torrent.py +++ b/deluge/tests/test_torrent.py @@ -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 @@ def test_connect_peer_port(self): 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