fix(download): reentra no acelerador na proxima tentativa apos queda

Achados do CodeRabbit no PR #1942.

O mais grave: com o erro classificado como transitorio, o codigo mantinha o
acelerador ligado mas caia direto no `snapshot_download` na MESMA tentativa. Se
esse download desse certo, o laco terminava e o manifesto do `.part` nunca era
reusado — exatamente o recomeco-do-zero que a correcao existe para impedir.

Agora o erro transitorio e propagado para o retry externo, cuja proxima
tentativa reentra no `_segmented_snapshot` e retoma do manifesto. A decisao
virou o helper puro `_segmented_retry_plan`, testavel direto (o laco mora dentro
de `install_model`, uma rota de ~200 linhas). A ultima tentativa fica reservada
para o caminho simples, entao o acelerador continua sem poder ser o motivo de um
install falhar de vez.

Tambem deste round de revisao:

- `Invoke-CimMethod ... Terminate` tinha o retorno descartado com `$null =`. O
  Win32_Process.Terminate reporta falha pelo ReturnValue, nao lancando: um kill
  negado por permissao era reportado como sucesso e a porta seguia presa. Agora
  o ReturnValue e validado, com exit 4 proprio e a mensagem carregando o codigo.
- O teste de concorrencia era vazio: o handler sincrono do MockTransport retorna
  antes de qualquer outra task rodar, entao `peak` nunca passava de 1 e a
  asserção `peak <= 4` passava sem exercitar o semaforo. Passou a segurar as
  requisicoes abertas com um asyncio.Event e a exigir `peak == 4` (verificado:
  com o semaforo afrouxado para 1000, o teste acusa 31).
- A doc dizia que OMNIVOICE_DOWNLOAD_MAX_WORKERS limita as faixas e que origem
  sem Range cai no snapshot_download. Nenhum dos dois: `_segmented_snapshot` nao
  passa `num_connections` (usa as 8 padrao) e origem sem Range vira stream unico
  dentro do proprio acelerador.
- Entradas de Highlights do CHANGELOG sem o `(#NNNN)` exigido.
This commit is contained in:
marreiradigital
2026-09-08 23:12:14 -04:00
parent d01fb5e7cc
commit 66f7ef8cfe
7 changed files with 158 additions and 42 deletions
+48 -20
View File
@@ -142,29 +142,57 @@ def test_small_file_still_single_segment():
def test_concurrency_stays_at_num_connections(tmp_path, monkeypatch):
"""Many bounded segments must not all fire at once."""
"""Many bounded segments must not all fire at once.
The handler has to HOLD requests open: a synchronous mock returns before any
other task is scheduled, so nothing ever overlaps and the assertion passes
without exercising the semaphore at all.
"""
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
connections = 4
assert len(_plan_segments(len(PAYLOAD), connections)) > connections, (
"test needs more segments than connections"
)
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
state = {"inflight": 0, "peak": 0}
async def _run():
saturated = asyncio.Event()
async def handler(request: httpx.Request) -> httpx.Response:
if request.method == "HEAD":
return httpx.Response(200, headers={
"content-length": str(len(PAYLOAD)), "accept-ranges": "bytes"})
state["inflight"] += 1
state["peak"] = max(state["peak"], state["inflight"])
try:
if state["inflight"] >= connections:
saturated.set()
# Hold the range open until the pool fills, so overlap is
# observable without sleeping. The timeout keeps an
# over-restrictive semaphore a failure instead of a hang.
try:
await asyncio.wait_for(saturated.wait(), timeout=1)
except asyncio.TimeoutError:
pass
lo, hi = request.headers["range"].replace("bytes=", "").split("-")
return httpx.Response(206, content=PAYLOAD[int(lo):int(hi) + 1])
finally:
state["inflight"] -= 1
async with httpx.AsyncClient(
transport=httpx.MockTransport(handler), follow_redirects=False
) as client:
await segmented_download(
"https://cdn.example.com/f.bin", dest, client=client,
expected_size=len(PAYLOAD), num_connections=connections,
)
asyncio.run(_run())
assert state["peak"] == connections, (
f"expected exactly {connections} ranges in flight, saw {state['peak']}"
)
with open(dest, "rb") as f:
assert f.read() == PAYLOAD