197 lines
6.0 KiB
Python
Raw Normal View History

2024-11-01 18:07:08 -03:00
import base64
2024-11-03 08:29:29 -03:00
import logging
2024-11-01 18:07:08 -03:00
import shutil
2024-12-04 19:04:57 -03:00
import socket
2024-11-01 18:07:08 -03:00
from dataclasses import dataclass
from io import BytesIO
from pathlib import Path
from typing import List, Optional, Self, Dict, Any
2024-11-01 18:07:08 -03:00
import pathvalidate
from deluge_client import DelugeRPCClient
from tenacity import retry, wait_exponential, stop_after_attempt
from tenacity.stop import stop_base
from tenacity.wait import wait_base
2024-11-01 18:07:08 -03:00
from torrentool.torrent import Torrent
from urllib3.util import Url
from benchmarks.core.experiments.experiments import ExperimentComponent
from benchmarks.core.network import DownloadHandle
from benchmarks.core.utils import await_predicate
from benchmarks.deluge.agent.client import DelugeAgentClient
2024-11-03 08:29:29 -03:00
logger = logging.getLogger(__name__)
2024-11-01 18:07:08 -03:00
@dataclass(frozen=True)
class DelugeMeta:
2024-11-05 12:18:47 -03:00
""":class:`DelugeMeta` represents the initial metadata required so that a :class:`DelugeNode`
can introduce a file into the network, becoming its initial seeder."""
2024-12-14 06:34:11 -03:00
2024-11-01 18:07:08 -03:00
name: str
announce_url: Url
class DelugeNode(ExperimentComponent):
2024-11-01 18:07:08 -03:00
def __init__(
2024-12-14 06:34:11 -03:00
self,
name: str,
volume: Path,
daemon_port: int,
agent_url: Url = Url(scheme="http", host="localhost", port=8000),
2024-12-14 06:34:11 -03:00
daemon_address: str = "localhost",
daemon_username: str = "user",
daemon_password: str = "password",
2024-11-01 18:07:08 -03:00
) -> None:
if not pathvalidate.is_valid_filename(name):
raise ValueError(f'Node name must be a valid filename (bad name: "{name}")')
self._name = name
self.downloads_root = volume / "downloads"
2024-11-01 18:07:08 -03:00
self._rpc: Optional[DelugeRPCClient] = None
self.daemon_args = {
2024-12-14 06:34:11 -03:00
"host": daemon_address,
"port": daemon_port,
"username": daemon_username,
"password": daemon_password,
2024-11-01 18:07:08 -03:00
}
self.agent = DelugeAgentClient(agent_url)
@property
def name(self) -> str:
return self._name
2024-11-01 18:07:08 -03:00
def wipe_all_torrents(self):
torrent_ids = list(self.rpc.core.get_torrents_status({}, []).keys())
if torrent_ids:
errors = self.rpc.core.remove_torrents(torrent_ids, remove_data=True)
if errors:
2024-12-14 06:34:11 -03:00
raise Exception(f"There were errors removing torrents: {errors}")
2024-11-01 18:07:08 -03:00
# Wipe download folder to get rid of files that got uploaded but failed
2024-12-05 16:30:15 -03:00
# seeding or deletes.
try:
shutil.rmtree(self.downloads_root)
2024-12-05 16:30:15 -03:00
except FileNotFoundError:
# If the call to remove_torrents succeeds, this might happen. Checking
# for existence won't protect you as the client might still delete the
# folder after your check, so this is the only sane way to do it.
pass
def genseed(
2024-12-14 06:34:11 -03:00
self,
size: int,
seed: int,
meta: DelugeMeta,
2024-11-01 18:07:08 -03:00
) -> Torrent:
torrent = self.agent.generate(size, seed, meta.name)
torrent.announce_urls = [str(meta.announce_url)]
2024-11-01 18:07:08 -03:00
self.rpc.core.add_torrent_file(
filename=f"{meta.name}.torrent",
2024-11-01 18:07:08 -03:00
filedump=self._b64dump(torrent),
options=dict(),
)
return torrent
2024-11-03 08:29:29 -03:00
def leech(self, handle: Torrent) -> DownloadHandle:
self.rpc.core.add_torrent_file(
2024-12-14 06:34:11 -03:00
filename=f"{handle.name}.torrent",
2024-11-03 08:29:29 -03:00
filedump=self._b64dump(handle),
options=dict(),
)
return DelugeDownloadHandle(
node=self,
torrent=handle,
)
2024-11-01 18:07:08 -03:00
def remove(self, handle: Torrent):
self.rpc.core.remove_torrent(handle.info_hash, remove_data=True)
2024-11-01 18:07:08 -03:00
def torrent_info(self, name: str) -> List[Dict[bytes, Any]]:
2024-12-14 06:34:11 -03:00
return list(self.rpc.core.get_torrents_status({"name": name}, []).values())
2024-11-01 18:07:08 -03:00
@property
def rpc(self) -> DelugeRPCClient:
if self._rpc is None:
self.connect()
return self._rpc
@retry(
stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=4, max=16)
)
2024-11-01 18:07:08 -03:00
def connect(self) -> Self:
return self._raw_connect()
def _raw_connect(self):
2024-11-01 18:07:08 -03:00
client = DelugeRPCClient(**self.daemon_args)
client.connect()
self._rpc = ResilientCallWrapper(
client,
wait_policy=wait_exponential(multiplier=1, min=4, max=16),
stop_policy=stop_after_attempt(5),
)
2024-11-01 18:07:08 -03:00
return self
def is_ready(self) -> bool:
try:
self._raw_connect()
return True
2024-12-04 19:04:57 -03:00
except (ConnectionRefusedError, socket.gaierror):
return False
2024-11-01 18:07:08 -03:00
@staticmethod
def _b64dump(handle: Torrent) -> bytes:
buffer = BytesIO()
buffer.write(handle.to_string())
return base64.b64encode(buffer.getvalue())
2024-11-03 08:29:29 -03:00
2024-12-06 14:18:30 -03:00
def __str__(self):
return f"DelugeNode({self.name}, {self.daemon_args['host']}:{self.daemon_args['port']})"
2024-11-03 08:29:29 -03:00
class ResilientCallWrapper:
def __init__(self, node: Any, wait_policy: wait_base, stop_policy: stop_base):
self.node = node
self.wait_policy = wait_policy
self.stop_policy = stop_policy
def __call__(self, *args, **kwargs):
@retry(wait=self.wait_policy, stop=self.stop_policy)
def _resilient_wrapper():
return self.node(*args, **kwargs)
return _resilient_wrapper()
def __getattr__(self, item):
return ResilientCallWrapper(
getattr(self.node, item),
wait_policy=self.wait_policy,
stop_policy=self.stop_policy,
)
2024-11-03 08:29:29 -03:00
class DelugeDownloadHandle(DownloadHandle):
def __init__(self, torrent: Torrent, node: DelugeNode) -> None:
self.node = node
self.torrent = torrent
def await_for_completion(self, timeout: float = 0) -> bool:
name = self.torrent.name
def _predicate():
2024-12-14 06:34:11 -03:00
response = self.node.rpc.core.get_torrents_status({"name": name}, [])
2024-11-03 08:29:29 -03:00
if len(response) > 1:
2024-12-14 06:34:11 -03:00
logger.warning(
f"Client has multiple torrents matching name {name}. Returning the first one."
)
2024-11-03 08:29:29 -03:00
status = list(response.values())[0]
2024-12-14 06:34:11 -03:00
return status[b"is_seed"]
return await_predicate(_predicate, timeout=timeout)