mirror of
https://github.com/unshackle-dl/unshackle.git
synced 2026-11-15 13:35:05 +00:00
perf: lazy-load services and proxy catalogues, faster namespace strip
load_xml strips namespaces without QName; services import on first use and proxy providers fetch their catalogue on first query; the font scan is skipped when no subtitle names a font and is cached per process. Serve CPU -40% and track requests -28% on 20 MB manifests; CLI startup 2.7 s -> 1.2 s; --list 27-46% faster across six services tested.
This commit is contained in:
@@ -3,6 +3,7 @@ split, the named or bare provider lookup, and the line that reports which proxy
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Iterator, Optional
|
||||
|
||||
@@ -188,10 +189,29 @@ def test_bare_query_skips_a_provider_that_raises(caplog: pytest.LogCaptureFixtur
|
||||
pick_proxy([Broken()], "us")
|
||||
|
||||
|
||||
def test_named_query_passes_the_provider_error_on() -> None:
|
||||
def test_named_query_passes_the_provider_error_on_as_a_value_error() -> None:
|
||||
class Broken:
|
||||
def get_proxy(self, query: str) -> str:
|
||||
raise ConnectionError("api down")
|
||||
|
||||
with pytest.raises(ConnectionError, match="api down"):
|
||||
with pytest.raises(ValueError, match="The proxy provider broken failed for us: api down") as raised:
|
||||
pick_proxy([Broken()], "us", "broken")
|
||||
assert isinstance(raised.value.__cause__, ConnectionError)
|
||||
|
||||
|
||||
def test_named_query_names_the_provider_for_a_bad_catalogue_response() -> None:
|
||||
class Broken:
|
||||
def get_proxy(self, query: str) -> str:
|
||||
raise json.JSONDecodeError("Expecting value", "", 0)
|
||||
|
||||
with pytest.raises(ValueError, match="The proxy provider broken failed for us: Expecting value"):
|
||||
pick_proxy([Broken()], "us", "broken")
|
||||
|
||||
|
||||
def test_named_query_passes_a_query_error_on_unchanged() -> None:
|
||||
class Strict:
|
||||
def get_proxy(self, query: str) -> str:
|
||||
raise ValueError("no server for us150")
|
||||
|
||||
with pytest.raises(ValueError, match="^no server for us150$"):
|
||||
pick_proxy([Strict()], "us150", "strict")
|
||||
|
||||
@@ -4,6 +4,7 @@ by a bare query and reported by a query that names it."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
@@ -15,17 +16,14 @@ from unshackle.core.proxies.resolve import Unavailable, find_provider, load_prox
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
CREDENTIALS = {"username": "a" * 24, "password": "b" * 24}
|
||||
LOGIN_CREDENTIALS = {"username": "someone@example.com", "password": "hunter2"}
|
||||
OTHERS = ("basic", "expressvpn", "protonvpn", "windscribevpn", "gluetun", "hola", "controld")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def nordvpn_down_surfshark_up(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def refuse() -> list[dict]:
|
||||
raise requests.ConnectionError("no route to api.nordvpn.com")
|
||||
|
||||
def nordvpn_misconfigured_surfshark_up(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
clusters = [{"countryCode": "US", "connectionName": "us-dal.prod.surfshark.com", "location": "Dallas"}]
|
||||
monkeypatch.setattr(config, "proxy_providers", {"nordvpn": CREDENTIALS, "surfsharkvpn": CREDENTIALS})
|
||||
monkeypatch.setattr(NordVPN, "get_countries", staticmethod(refuse))
|
||||
monkeypatch.setattr(config, "proxy_providers", {"nordvpn": LOGIN_CREDENTIALS, "surfsharkvpn": CREDENTIALS})
|
||||
monkeypatch.setattr(SurfsharkVPN, "get_countries", staticmethod(lambda: clusters))
|
||||
|
||||
|
||||
@@ -49,7 +47,7 @@ def test_query_naming_another_provider_still_works() -> None:
|
||||
|
||||
@pytest.mark.parametrize("name", ["nordvpn", "NordVPN"])
|
||||
def test_query_naming_the_failed_provider_reports_why(name: str) -> None:
|
||||
with pytest.raises(ValueError, match="NordVPN could not load: no route to api.nordvpn.com"):
|
||||
with pytest.raises(ValueError, match="NordVPN could not load: NordVPN needs its Service Credentials"):
|
||||
resolve_proxy(f"{name}:us", load_proxy_providers(OTHERS))
|
||||
|
||||
|
||||
@@ -59,10 +57,29 @@ def test_query_naming_an_unknown_provider_lists_the_loaded_ones() -> None:
|
||||
|
||||
|
||||
def test_raise_errors_raises_the_load_failure() -> None:
|
||||
with pytest.raises(requests.ConnectionError):
|
||||
with pytest.raises(ValueError, match="Service Credentials"):
|
||||
load_proxy_providers(OTHERS, raise_errors=True)
|
||||
|
||||
|
||||
def test_catalogue_is_fetched_on_first_use_and_a_failed_fetch_fails_only_its_query(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
calls = []
|
||||
|
||||
def refuse() -> list[dict]:
|
||||
calls.append(1)
|
||||
raise requests.ConnectionError("no route to api.nordvpn.com")
|
||||
|
||||
monkeypatch.setattr(config, "proxy_providers", {"nordvpn": CREDENTIALS, "surfsharkvpn": CREDENTIALS})
|
||||
monkeypatch.setattr(NordVPN, "get_countries", staticmethod(refuse))
|
||||
nordvpn, surfshark = load_proxy_providers(OTHERS)
|
||||
assert isinstance(nordvpn, NordVPN) and not calls
|
||||
|
||||
assert resolve_proxy("us", [nordvpn, surfshark]).endswith("@us-dal.prod.surfshark.com:443")
|
||||
with pytest.raises(ValueError, match="nordvpn failed for us: no route to api.nordvpn.com"):
|
||||
resolve_proxy("nordvpn:us", [nordvpn, surfshark])
|
||||
|
||||
|
||||
def test_bare_query_order_follows_config_key_order_with_controld_last(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from unshackle.core.proxies import ControlD
|
||||
|
||||
@@ -88,3 +105,71 @@ def test_rest_path_loads_windscribe_but_not_gluetun(monkeypatch: pytest.MonkeyPa
|
||||
assert any(isinstance(p, WindscribeVPN) for p in providers)
|
||||
assert not any(isinstance(p, Gluetun) or getattr(p, "name", None) == "Gluetun" for p in providers)
|
||||
assert resolve_proxy("windscribevpn:us", providers).endswith("@us-central-1.example:443")
|
||||
|
||||
|
||||
def build_controld() -> Any:
|
||||
from unshackle.core.proxies import ControlD
|
||||
|
||||
return ControlD(token="t")
|
||||
|
||||
|
||||
def build_hola(monkeypatch: pytest.MonkeyPatch) -> Any:
|
||||
from unshackle.core import binaries
|
||||
from unshackle.core.proxies import Hola
|
||||
|
||||
monkeypatch.setattr(binaries, "HolaProxy", "hola-proxy")
|
||||
return Hola()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("name", "fetch", "attr"),
|
||||
[
|
||||
("NordVPN", "get_countries", "countries"),
|
||||
("WindscribeVPN", "get_countries", "countries"),
|
||||
("SurfsharkVPN", "get_countries", "countries"),
|
||||
("ControlD", "get_locations", "catalogue"),
|
||||
("Hola", "get_countries", "countries"),
|
||||
],
|
||||
)
|
||||
def test_construction_makes_no_request_and_the_first_read_fetches_once(
|
||||
name: str, fetch: str, attr: str, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
from unshackle.core import proxies
|
||||
|
||||
cls = getattr(proxies, name)
|
||||
calls: list[int] = []
|
||||
|
||||
def counted(*args: Any) -> list[dict]:
|
||||
calls.append(1)
|
||||
return [{"country": "US"}]
|
||||
|
||||
is_static = isinstance(vars(cls)[fetch], staticmethod)
|
||||
monkeypatch.setattr(cls, fetch, staticmethod(counted) if is_static else counted)
|
||||
if name == "ControlD":
|
||||
provider = build_controld()
|
||||
elif name == "Hola":
|
||||
provider = build_hola(monkeypatch)
|
||||
else:
|
||||
provider = cls(**CREDENTIALS)
|
||||
assert not calls
|
||||
|
||||
assert getattr(provider, attr) == [{"country": "US"}]
|
||||
getattr(provider, attr)
|
||||
assert calls == [1]
|
||||
|
||||
|
||||
def test_health_probe_reads_each_proxy_catalogue(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from unshackle.core.api.handlers import run_health_checks
|
||||
|
||||
calls: list[int] = []
|
||||
|
||||
def countries() -> list[dict]:
|
||||
calls.append(1)
|
||||
return [{"serverCount": 3}]
|
||||
|
||||
monkeypatch.setattr(config, "key_vaults", [])
|
||||
monkeypatch.setattr(config, "proxy_providers", {"nordvpn": CREDENTIALS})
|
||||
monkeypatch.setattr(NordVPN, "get_countries", staticmethod(countries))
|
||||
detail = {c["id"]: c for c in run_health_checks()}["proxies"]["detail"]
|
||||
assert calls == [1]
|
||||
assert "NordVPN (1 Country (3 Servers))" in detail
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"""Services import on first use: a command pays only for the service it runs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
import pytest
|
||||
from aiohttp import web
|
||||
|
||||
from unshackle.core import binaries, services
|
||||
from unshackle.core.config import config
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
EXAMPLE_SOURCE = """\
|
||||
import click
|
||||
|
||||
|
||||
class EXAMPLE:
|
||||
ALIASES = ("ex",)
|
||||
|
||||
@staticmethod
|
||||
def get_binaries():
|
||||
return [{"name": "ExampleTool", "desc": "example dependency"}]
|
||||
|
||||
@staticmethod
|
||||
@click.command()
|
||||
def cli():
|
||||
pass
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def two_services(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
for tag, body in (("EXAMPLE", EXAMPLE_SOURCE), ("OTHER", "raise ImportError('boom')\n")):
|
||||
(tmp_path / tag).mkdir()
|
||||
(tmp_path / tag / "__init__.py").write_text(body)
|
||||
monkeypatch.setattr(services, "SERVICES", [tmp_path / t / "__init__.py" for t in ("EXAMPLE", "OTHER")])
|
||||
monkeypatch.setattr(services, "MODULES", {})
|
||||
monkeypatch.setattr(services, "ALIASES", {})
|
||||
monkeypatch.setattr(services, "LOAD_ERRORS", [])
|
||||
monkeypatch.setattr(services, "SHADOWED", [])
|
||||
monkeypatch.setattr(services, "DIRTY_REPOS", [])
|
||||
monkeypatch.setattr(services, "SUMMARY_LOGGED", False)
|
||||
monkeypatch.setattr(binaries, "_registered_binaries", {})
|
||||
monkeypatch.setattr(binaries, "__binaries", dict(getattr(binaries, "__binaries")))
|
||||
yield
|
||||
for name in ("EXAMPLE", "OTHER"):
|
||||
sys.modules.pop(name, None)
|
||||
|
||||
|
||||
def test_a_tag_imports_only_its_own_service(two_services: None) -> None:
|
||||
assert services.Services.get_tag("example") == "EXAMPLE"
|
||||
assert services.MODULES == {}
|
||||
|
||||
assert services.Services.load("EXAMPLE").__name__ == "EXAMPLE"
|
||||
assert list(services.MODULES) == ["EXAMPLE"] and not services.LOAD_ERRORS
|
||||
assert [d["name"] for d in binaries.get_registered_dependencies()] == ["ExampleTool"]
|
||||
|
||||
|
||||
def test_an_alias_imports_services_until_one_matches(two_services: None) -> None:
|
||||
assert services.Services.get_tag("ex") == "EXAMPLE"
|
||||
assert list(services.MODULES) == ["EXAMPLE"]
|
||||
|
||||
|
||||
def test_a_failed_import_fails_only_its_own_tag_and_is_not_retried(two_services: None) -> None:
|
||||
with pytest.raises(KeyError):
|
||||
services.Services.load("OTHER")
|
||||
assert len(services.LOAD_ERRORS) == 1 and services.LOAD_ERRORS[0].startswith("OTHER: ")
|
||||
services.ensure_loaded("OTHER")
|
||||
assert len(services.LOAD_ERRORS) == 1
|
||||
|
||||
services.check_load_errors("EXAMPLE")
|
||||
with pytest.raises(click.ClickException, match="Failed to load 1 service"):
|
||||
services.check_load_errors("OTHER")
|
||||
|
||||
|
||||
def test_get_command_runs_a_good_service_beside_a_broken_one(two_services: None) -> None:
|
||||
group = services.Services(name="dl")
|
||||
ctx = click.Context(group)
|
||||
with pytest.raises(click.ClickException, match="OTHER: failed to import"):
|
||||
group.get_command(ctx, "OTHER")
|
||||
assert group.get_command(ctx, "EXAMPLE").name == "EXAMPLE"
|
||||
|
||||
|
||||
def test_get_tag_does_not_wait_for_a_reload_on_a_failed_tag(two_services: None) -> None:
|
||||
services.ensure_loaded("EXAMPLE")
|
||||
services.ensure_loaded("OTHER")
|
||||
held, release = threading.Event(), threading.Event()
|
||||
|
||||
def reload() -> None:
|
||||
with services.RELOAD_LOCK:
|
||||
held.set()
|
||||
release.wait()
|
||||
|
||||
holder = threading.Thread(target=reload)
|
||||
holder.start()
|
||||
try:
|
||||
assert held.wait(5)
|
||||
result: list[str] = []
|
||||
lookup = threading.Thread(target=lambda: result.append(services.Services.get_tag("nosuch")))
|
||||
lookup.start()
|
||||
lookup.join(5)
|
||||
assert result == ["nosuch"]
|
||||
finally:
|
||||
release.set()
|
||||
holder.join()
|
||||
|
||||
|
||||
def test_load_all_imports_every_service(two_services: None) -> None:
|
||||
services.load_all()
|
||||
assert list(services.MODULES) == ["EXAMPLE"]
|
||||
assert services.ALIASES == {"EXAMPLE": ("ex",)}
|
||||
assert services.failed_tags(services.LOAD_ERRORS) == {"OTHER"}
|
||||
|
||||
|
||||
def test_env_dependencies_list_every_service_binary(two_services: None) -> None:
|
||||
from unshackle.commands.env import get_dependencies
|
||||
|
||||
assert "ExampleTool" in {dep["name"] for dep in get_dependencies()}
|
||||
|
||||
|
||||
def test_serve_startup_imports_every_service(two_services: None, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from unshackle.commands.serve import _install_service_refresh
|
||||
|
||||
monkeypatch.setattr(config, "serve", {})
|
||||
_install_service_refresh(web.Application())
|
||||
assert list(services.MODULES) == ["EXAMPLE"]
|
||||
|
||||
|
||||
def test_a_run_of_one_tag_names_its_duplicate(two_services: None, caplog: pytest.LogCaptureFixture) -> None:
|
||||
services.SHADOWED[:] = [
|
||||
"EXAMPLE: using /a/EXAMPLE, ignoring /b/EXAMPLE",
|
||||
"OTHER: using /a/OTHER, ignoring /b/OTHER",
|
||||
]
|
||||
with caplog.at_level(logging.INFO, logger="services"):
|
||||
services.check_load_errors("EXAMPLE")
|
||||
assert "EXAMPLE: 1 duplicate(s) ignored" in caplog.text
|
||||
assert "OTHER" not in caplog.text
|
||||
Reference in New Issue
Block a user