Files
VoiceStudio/tests/test_fdl_segmented_download.py
T
marreiradigital 0d3fb07c1f fix(download): segmenta em blocos limitados e retoma o acelerador
O downloader segmentado gravava progresso no manifesto apenas quando um
segmento INTEIRO terminava, e dimensionava os segmentos como
tamanho/num_connections. Num blob de 806 MB isso dava 8 segmentos de ~100 MB:
numa conexão que cai a cada ~50 MB nenhum segmento jamais completava, o
manifesto nunca era escrito e cada tentativa recomeçava do zero.

Pior, o acelerador só rodava na PRIMEIRA tentativa (`_attempt == 1`), então
depois da primeira queda todas as retentativas iam para o `snapshot_download`
e o `.part` acumulado ficava órfão para sempre.

Agora os segmentos são limitados a 16 MB e a concorrência passa a ser
controlada por semáforo (antes vinha da própria contagem de segmentos), e o
acelerador é preservado entre tentativas quando o erro é de rede — reusando
`_is_retryable_download_error`, que já é a fonte única dessa classificação.
Ele só é desligado de vez quando a falha NÃO é transitória, ou seja, quando o
acelerador de fato não serve naquele host.

Reproduzido em rede real: `peer closed connection without sending complete
message body (received 54260979, expected 100708200)`.
2026-09-08 22:45:31 -04:00

205 lines
7.8 KiB
Python

"""FDL-08: segmented (multi-connection) downloader — correctness + auth safety.
Each test runs a single self-contained coroutine via ``asyncio.run`` (fresh
event loop per call). Do NOT use ``asyncio.get_event_loop()`` here: in the full
suite an earlier async test can leave the global loop closed, which would make
these RuntimeError even though they pass standalone.
"""
from __future__ import annotations
import asyncio
import os
import httpx
import pytest
import services.segmented_download as sd
from services.segmented_download import (
segmented_download,
DownloadCancelled,
_plan_segments,
_MAX_SEGMENT_BYTES,
)
PAYLOAD = bytes((i % 256) for i in range(1_000_000)) # 1 MB deterministic body
def _ranged_handler(payload=PAYLOAD, *, accept_ranges=True, record=None):
"""A mock origin that honours Range requests over `payload`."""
def handler(request: httpx.Request) -> httpx.Response:
if record is not None:
record.append(request)
if request.method == "HEAD":
h = {"content-length": str(len(payload))}
if accept_ranges:
h["accept-ranges"] = "bytes"
return httpx.Response(200, headers=h)
rng = request.headers.get("range")
if rng and accept_ranges:
lo, hi = rng.replace("bytes=", "").split("-")
lo, hi = int(lo), int(hi)
return httpx.Response(206, content=payload[lo:hi + 1])
return httpx.Response(200, content=payload)
return handler
def _client(handler):
return httpx.AsyncClient(transport=httpx.MockTransport(handler), follow_redirects=False)
def _download(handler, dest, **kw):
"""Run one segmented_download against a mock origin in a fresh event loop."""
url = kw.pop("url", "https://cdn.example.com/f.bin")
async def _do():
async with _client(handler) as client:
return await segmented_download(url, dest, client=client, **kw)
return asyncio.run(_do())
def test_parallel_ranges_reassemble_exactly(tmp_path):
dest = str(tmp_path / "model.bin")
_download(_ranged_handler(), dest, expected_size=len(PAYLOAD), num_connections=8)
with open(dest, "rb") as f:
assert f.read() == PAYLOAD
assert not os.path.exists(dest + ".part")
assert not os.path.exists(dest + ".part.done")
def test_single_stream_fallback_when_no_range(tmp_path):
dest = str(tmp_path / "f.bin")
_download(_ranged_handler(accept_ranges=False), dest, expected_size=len(PAYLOAD))
with open(dest, "rb") as f:
assert f.read() == PAYLOAD
def test_auth_header_never_sent_to_cdn_host(tmp_path):
record = []
dest = str(tmp_path / "f.bin")
_download(_ranged_handler(record=record), dest,
url="https://cdn.cloudfront.net/blob", # NOT a huggingface.co host
token="hf_secrettoken", expected_size=len(PAYLOAD))
assert record
assert all("authorization" not in {k.lower() for k in r.headers} for r in record), \
"Authorization must never be sent to a non-huggingface.co host"
def test_auth_header_sent_to_hf_host(tmp_path):
record = []
dest = str(tmp_path / "f.bin")
_download(_ranged_handler(record=record), dest,
url="https://huggingface.co/api/x/resolve/main/f",
token="hf_tok", expected_size=len(PAYLOAD))
assert any(r.headers.get("authorization") == "Bearer hf_tok" for r in record)
def test_size_mismatch_raises(tmp_path):
dest = str(tmp_path / "f.bin")
with pytest.raises(ValueError):
_download(_ranged_handler(), dest, expected_size=len(PAYLOAD) + 999)
assert not os.path.exists(dest)
def test_cancel_raises_and_leaves_no_commit(tmp_path):
dest = str(tmp_path / "f.bin")
with pytest.raises(DownloadCancelled):
_download(_ranged_handler(), dest, expected_size=len(PAYLOAD), cancel_check=lambda: True)
assert not os.path.exists(dest)
def test_on_bytes_reports_total(tmp_path):
dest = str(tmp_path / "f.bin")
seen = []
_download(_ranged_handler(), dest, expected_size=len(PAYLOAD), on_bytes=lambda d: seen.append(d))
assert sum(seen) == len(PAYLOAD)
# ── #1224 follow-up: bounded segments so a flaky link makes progress ────────
#
# Progress is committed to the manifest only when a WHOLE segment lands, so the
# segment size is also the most bytes a dropped connection can throw away.
# Sizing segments as size/num_connections made that ~100 MB on an 800 MB blob:
# on a link dropping every ~50 MB no segment ever completed, the manifest was
# never written, and every retry restarted from zero.
def test_large_file_is_split_into_bounded_segments():
"""Fail-before: the old plan returned 8 segments of ~100 MB for this size."""
size = 805_665_628 # the k2-fsa/OmniVoice blob that reproduced the stall
segs = _plan_segments(size, 8)
assert max(e - s + 1 for s, e in segs) <= _MAX_SEGMENT_BYTES
assert len(segs) > 8, "segment count must not be capped by num_connections"
# Exact cover: no gap, no overlap, no byte past the end.
assert segs[0][0] == 0 and segs[-1][1] == size - 1
assert all(segs[i][1] + 1 == segs[i + 1][0] for i in range(len(segs) - 1))
def test_small_file_still_single_segment():
"""The cap must not shard tiny files into per-request overhead."""
assert len(_plan_segments(1_000_000, 8)) == 1
def test_concurrency_stays_at_num_connections(tmp_path, monkeypatch):
"""Many bounded segments must not all fire at once."""
monkeypatch.setattr(sd, "_MIN_SEGMENT_BYTES", 16 * 1024)
monkeypatch.setattr(sd, "_MAX_SEGMENT_BYTES", 32 * 1024)
inflight = 0
peak = 0
def handler(request: httpx.Request) -> httpx.Response:
nonlocal inflight, peak
if request.method == "HEAD":
return httpx.Response(200, headers={
"content-length": str(len(PAYLOAD)), "accept-ranges": "bytes"})
inflight += 1
peak = max(peak, inflight)
try:
lo, hi = request.headers["range"].replace("bytes=", "").split("-")
return httpx.Response(206, content=PAYLOAD[int(lo):int(hi) + 1])
finally:
inflight -= 1
dest = str(tmp_path / "m.bin")
_download(handler, dest, expected_size=len(PAYLOAD), num_connections=4)
assert len(_plan_segments(len(PAYLOAD), 4)) > 4, "test needs more segments than connections"
assert peak <= 4
with open(dest, "rb") as f:
assert f.read() == PAYLOAD
def test_dropped_connection_resumes_from_manifest(tmp_path, monkeypatch):
"""A drop must cost one segment, not the whole file."""
monkeypatch.setattr(sd, "_MIN_SEGMENT_BYTES", 16 * 1024)
monkeypatch.setattr(sd, "_MAX_SEGMENT_BYTES", 32 * 1024)
dest = str(tmp_path / "m.bin")
served = []
fail_after = {"n": 3}
def handler(request: httpx.Request) -> httpx.Response:
if request.method == "HEAD":
return httpx.Response(200, headers={
"content-length": str(len(PAYLOAD)), "accept-ranges": "bytes"})
if fail_after["n"] > 0:
fail_after["n"] -= 1
if fail_after["n"] == 0:
raise httpx.RemoteProtocolError("peer closed connection", request=request)
lo, hi = request.headers["range"].replace("bytes=", "").split("-")
served.append(int(hi) - int(lo) + 1)
return httpx.Response(206, content=PAYLOAD[int(lo):int(hi) + 1])
with pytest.raises(httpx.RemoteProtocolError):
_download(handler, dest, expected_size=len(PAYLOAD), num_connections=2)
assert os.path.exists(dest + ".part.done"), "completed segments must be committed"
before = sum(served)
assert before > 0
_download(handler, dest, expected_size=len(PAYLOAD), num_connections=2)
with open(dest, "rb") as f:
assert f.read() == PAYLOAD
# Resumed, not restarted: total bytes served stay below two full copies.
assert sum(served) < 2 * len(PAYLOAD)
assert sum(served) >= len(PAYLOAD)