Files
antonio 6ea785576f feat(B32): fila como write-ahead log, com migracao do recovery.json (#230)
Fecha o que ficou aberto na #217. Aquela PR entregava o modulo sem ligar nada;
o bloqueio declarado era "mudar todos os call sites e decidir a migracao".

Nenhum call site mudou. A API publica do recovery.rs continua igual
(init_from_disk, persist, remove, list, clear_all) e so o armazenamento por
baixo trocou. Era o caminho mais seguro e nao tinha sido tentado.

O formato anterior reescrevia o arquivo inteiro a cada mudanca, entao um crash
no meio da escrita podia levar a fila toda. Agora anexa uma linha com fsync por
registro: um crash so pode corromper o ultimo, e o decode_log descarta a linha
truncada e mantem o resto. Verificado com SIGKILL de verdade pelo
scripts/wal-kill9.py: 202 registros integros, exatamente 1 truncado.

Achado que so apareceu ao ligar: o Enqueued do WAL nao tinha `referer`, que
existe no RecoveryItem desde sempre. Migrar como estava teria descartado o
campo em silencio, e o download so falharia depois, no site que exige o header.
Campo adicionado em 6 pontos, com teste que reprova se ele parar de propagar.

A migracao do recovery.json roda uma vez, ordena por id (o JSON nao guardava
posicao, e inventar ordem embaralharia a fila de quem atualiza) e renomeia o
original para .json.migrated em vez de apagar.

A producao chama a mesma funcao que o teste exercita. Na primeira versao o
json_para_registros era usado so no teste e o codigo real reparseava por fora —
o portao de clippy pegou como dead_code, e o teste estaria provando algo que
ninguem executa.

609 testes, 0 falhando. Clippy 92, nenhum novo.
2026-07-29 16:06:07 -03:00

36 lines
1.5 KiB
Python

#!/usr/bin/env python3
"""Gera um WAL cortado por um SIGKILL de verdade.
Nao e simulacao: sobe um processo filho que escreve o log com fsync a cada
registro, e o mata com SIGKILL exatamente enquanto o ultimo registro esta pela
metade. O arquivo resultante e o que o kernel deixou.
Existe para produzir a fixture do teste `recupera_a_fila_de_um_arquivo_cortado_
por_kill9_real` em src-tauri/src/core/queue_wal.rs, e para poder refaze-la
quando o formato do registro mudar.
python3 scripts/wal-kill9.py
Imprime o caminho do WAL gerado. Resultado esperado: N registros integros e
exatamente 1 truncado.
"""
import json, os, subprocess, sys, tempfile, signal, time
d = tempfile.mkdtemp(prefix="omniget-wal-")
wal = os.path.join(d, "queue.wal")
child = f'''
import json, os, sys, time
f = open({wal!r}, "ab", buffering=0)
for i in range(200):
rec = {{"op":"enqueued","id":i,"url":f"https://example.com/{{i}}","title":f"v{{i}}",
"platform":"youtube","output_dir":"/downloads","position":i}}
f.write((json.dumps(rec)+"\\n").encode()); os.fsync(f.fileno())
f.write(json.dumps({{"op":"started","id":5}}).encode()+b"\\n"); os.fsync(f.fileno())
f.write(json.dumps({{"op":"completed","id":7,"file_path":"/x.mp4"}}).encode()+b"\\n"); os.fsync(f.fileno())
sys.stdout.write("READY\\n"); sys.stdout.flush()
f.write(b'{{"op":"progress","id":5,"perc'); os.fsync(f.fileno())
time.sleep(30)
'''
p = subprocess.Popen([sys.executable,"-c",child], stdout=subprocess.PIPE)
p.stdout.readline(); time.sleep(0.3); os.kill(p.pid, signal.SIGKILL); p.wait()
print(wal)