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 logging
import os import os
import socket import socket
import time
from typing import Optional
from urllib.parse import urlparse from urllib.parse import urlparse
from twisted.internet.defer import Deferred, DeferredList from twisted.internet.defer import Deferred, DeferredList
@ -234,7 +236,8 @@ class Torrent:
self.handle = handle self.handle = handle
self.magnet = magnet 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.torrent_info = self.handle.torrent_file()
self.has_metadata = self.status.has_metadata self.has_metadata = self.status.has_metadata
@ -267,7 +270,6 @@ class Torrent:
self.prev_status = {} self.prev_status = {}
self.waiting_on_folder_rename = [] self.waiting_on_folder_rename = []
self.update_status(self.handle.status())
self._create_status_funcs() self._create_status_funcs()
self.set_options(self.options) self.set_options(self.options)
self.update_state() self.update_state()
@ -641,7 +643,7 @@ class Torrent:
def update_state(self): def update_state(self):
"""Updates the state, based on libtorrent's torrent state""" """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() session_paused = component.get('Core').session.is_paused()
old_state = self.state old_state = self.state
self.set_status_message() self.set_status_message()
@ -709,7 +711,7 @@ class Torrent:
restart_to_resume (bool, optional): Prevent resuming clearing the error, only restarting restart_to_resume (bool, optional): Prevent resuming clearing the error, only restarting
session can resume. session can resume.
""" """
status = self.handle.status() status = self.get_lt_status()
self._set_handle_flags( self._set_handle_flags(
flag=lt.torrent_flags.auto_managed, flag=lt.torrent_flags.auto_managed,
set_flag=False, set_flag=False,
@ -1024,7 +1026,7 @@ class Torrent:
dict: a dictionary of the status keys and their values dict: a dictionary of the status keys and their values
""" """
if update: if update:
self.update_status(self.handle.status()) self.get_lt_status()
if all_keys: if all_keys:
keys = list(self.status_funcs) keys = list(self.status_funcs)
@ -1054,13 +1056,35 @@ class Torrent:
return status_dict 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. """Updates the cached status.
Args: 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): def _create_status_funcs(self):
"""Creates the functions for getting torrent status""" """Creates the functions for getting torrent status"""

View File

@ -279,11 +279,6 @@ class TorrentManager(component.Component):
'Paused', 'Paused',
'Queued', '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 ( if (
torrent.get_ratio() >= torrent.options['stop_ratio'] torrent.get_ratio() >= torrent.options['stop_ratio']
and torrent.is_finished and torrent.is_finished
@ -291,7 +286,7 @@ class TorrentManager(component.Component):
if torrent.options['remove_at_ratio']: if torrent.options['remove_at_ratio']:
self.remove(torrent_id) self.remove(torrent_id)
break break
if not torrent.handle.status().paused: if not torrent.status.paused:
torrent.pause() torrent.pause()
def __getitem__(self, torrent_id): def __getitem__(self, torrent_id):
@ -1359,10 +1354,8 @@ class TorrentManager(component.Component):
torrent.set_tracker_status('Announce OK') torrent.set_tracker_status('Announce OK')
# Check for peer information from the tracker, if none then send a scrape request. # Check for peer information from the tracker, if none then send a scrape request.
if ( torrent.get_lt_status()
alert.handle.status().num_complete == -1 if torrent.status.num_complete == -1 or torrent.status.num_incomplete == -1:
or alert.handle.status().num_incomplete == -1
):
torrent.scrape_tracker() torrent.scrape_tracker()
def on_alert_tracker_announce(self, alert): def on_alert_tracker_announce(self, alert):
@ -1612,7 +1605,7 @@ class TorrentManager(component.Component):
except RuntimeError: except RuntimeError:
continue continue
if torrent_id in self.torrents: 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()) 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. # the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details. # See LICENSE for more details.
# #
import itertools
import os import os
import time import time
from base64 import b64encode from base64 import b64encode
@ -356,3 +356,20 @@ class TestTorrent(BaseTestCase):
self.torrent = Torrent(handle, {}) self.torrent = Torrent(handle, {})
assert not self.torrent.connect_peer('127.0.0.1', 'text') assert not self.torrent.connect_peer('127.0.0.1', 'text')
assert self.torrent.connect_peer('127.0.0.1', '1234') 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