fix(feed): backup rebuild non-fatale — copy2→copyfile, il .bak non abortisce piu' il feed live

Il feed BTC/ETH del book era congelato 7 giorni (2026-07-08 -> 07-15): rebuild_history.py
usava shutil.copy2 per il .prebuild.bak; copy2=copyfile+copystat e copystat (os.utime/chmod
con valori espliciti) richiede la PROPRIETA' del file, non basta il group-write. I .bak sono
root:adriano, il cron gira come adriano -> EPERM ogni notte, abortendo PRIMA della scrittura
del feed (data/raw/*.parquet fermo a Jul 8). Il book ha ri-girato ogni ora su barra vecchia
(TP01 1d non ri-valutato); nessuna perdita (segnale fresco == tenuto) ma cieco 7g.

Fix: backup best-effort = shutil.copyfile (solo contenuto, no copystat) in try/except OSError.
Un .bak difensivo non deve mai poter bloccare la scrittura del feed live.

Verificato: rebuild rigirato -> feed a 2026-07-15, audit cross-venue BTC 1.9/ETH 2.0 bps,
file ora adriano:adriano (auto-sana), book dry-run legge ultima barra 2026-07-15 -> HOLD.

Diario: docs/diary/2026-07-15-feed-freeze-rebuild-copystat.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-15 18:37:42 +00:00
parent b691f48f43
commit 9fa3e6a0be
2 changed files with 135 additions and 1 deletions
+9 -1
View File
@@ -186,7 +186,15 @@ def build(asset: str, write: bool, smoke: bool) -> None:
for tf, d in built.items():
path = _parquet_path(asset, tf)
if path.exists():
shutil.copy2(path, BACKUP / f"{asset.lower()}_{tf}.parquet.prebuild.bak")
# Backup best-effort: copyfile (solo contenuto, NO copystat).
# copy2 copiava anche i metadati -> os.utime/chmod su un .bak di proprieta'
# altrui (es. root) fallisce con EPERM per un membro-gruppo e ABORTIVA il
# rebuild del feed live (incidente 2026-07-15: feed fermo 7g). Un backup e'
# difensivo: un suo errore non deve MAI bloccare la scrittura del feed.
try:
shutil.copyfile(path, BACKUP / f"{asset.lower()}_{tf}.parquet.prebuild.bak")
except OSError as e:
print(f" WARN backup {path.name} saltato (non fatale): {e}")
tmp = path.with_suffix(".parquet.tmp")
d.to_parquet(tmp, index=False)
tmp.replace(path)