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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user