137 lines
4.7 KiB
Python
137 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Alt-Paare und verwaiste Live-Läufe finden. Standard: Probelauf.
|
|
|
|
python scripts/laeufe_reparieren.py
|
|
python scripts/laeufe_reparieren.py --apply
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from grotti import jobs, store # noqa: E402
|
|
from grotti.config import load # noqa: E402
|
|
from grotti.db import connect, prepare # noqa: E402
|
|
|
|
|
|
def find_pairs(conn):
|
|
return conn.execute(
|
|
"""
|
|
SELECT a.id AS pred_id, b.id AS succ_id,
|
|
a.printer_host, a.filename,
|
|
a.state AS pred_state, b.state AS succ_state,
|
|
a.duration_s AS pred_dur, b.duration_s AS succ_dur,
|
|
a.ended_at, b.started_at,
|
|
a.deducted AS pred_deducted, b.deducted AS succ_deducted,
|
|
a.chargen_nr AS pred_chargen, b.chargen_nr AS succ_chargen
|
|
FROM runs a
|
|
JOIN runs b
|
|
ON a.printer_host = b.printer_host
|
|
AND a.filename = b.filename
|
|
AND a.id <> b.id
|
|
AND b.started_at BETWEEN a.ended_at - interval '30 seconds'
|
|
AND a.ended_at + interval '5 seconds'
|
|
WHERE a.state IN ('cancelled', 'interrupted', 'error')
|
|
AND a.ended_at IS NOT NULL
|
|
AND a.moonraker_job_id NOT LIKE 'live:%%'
|
|
AND b.moonraker_job_id NOT LIKE 'live:%%'
|
|
AND COALESCE(b.duration_s, 0) > 60
|
|
AND COALESCE(b.duration_s, 0) >= COALESCE(a.duration_s, 0) - 30
|
|
ORDER BY a.ended_at DESC
|
|
"""
|
|
).fetchall()
|
|
|
|
|
|
def find_orphan_live(conn):
|
|
return conn.execute(
|
|
"""
|
|
SELECT id, printer_host, filename, state, started_at, ended_at, duration_s, chargen_nr
|
|
FROM runs
|
|
WHERE moonraker_job_id LIKE 'live:%%'
|
|
AND state IN ('interrupted', 'completed', 'cancelled')
|
|
ORDER BY COALESCE(ended_at, started_at) DESC
|
|
LIMIT 100
|
|
"""
|
|
).fetchall()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Läufe nach Filament-Resume reparieren")
|
|
parser.add_argument("--apply", action="store_true", help="Wirklich zusammenlegen")
|
|
args = parser.parse_args()
|
|
cfg = load()
|
|
prepare(cfg["database_url"])
|
|
with connect(cfg["database_url"]) as conn:
|
|
pairs = find_pairs(conn)
|
|
orphans = find_orphan_live(conn)
|
|
print("=== Verdachtspaare (Beweis: Dauer fortgesetzt, Lücke ≤ 5 s) ===")
|
|
for row in pairs:
|
|
print(json.dumps({k: str(v) for k, v in dict(row).items()}, ensure_ascii=False))
|
|
print("%d Paare" % len(pairs))
|
|
print("=== Verwaiste live:-Läufe ===")
|
|
for row in orphans:
|
|
print(json.dumps({k: str(v) for k, v in dict(row).items()}, ensure_ascii=False))
|
|
print("%d orphan" % len(orphans))
|
|
if not args.apply:
|
|
print("Probelauf. Mit --apply werden Paare verbunden.")
|
|
conn.rollback()
|
|
return
|
|
merged = 0
|
|
for row in pairs:
|
|
try:
|
|
jobs.manual_merge_runs(
|
|
conn, row["pred_id"], row["succ_id"], confirm=True
|
|
)
|
|
merged += 1
|
|
print("verbunden", row["pred_id"], "+", row["succ_id"])
|
|
except Exception as exc:
|
|
print("Fehler", row["pred_id"], row["succ_id"], exc)
|
|
conn.rollback()
|
|
# Orphans nur an History derselben Datei hängen (kein Host-Fallback).
|
|
# adopt_live_run nimmt geschlossene live: nur ≤ 2 h — ältere bleiben liegen.
|
|
cleaned = 0
|
|
skipped = 0
|
|
for row in orphans:
|
|
fname = (row.get("filename") or "").strip()
|
|
if not fname:
|
|
skipped += 1
|
|
continue
|
|
hist = conn.execute(
|
|
"""
|
|
SELECT * FROM runs
|
|
WHERE printer_host = %s
|
|
AND filename = %s
|
|
AND moonraker_job_id NOT LIKE 'live:%%'
|
|
AND state IN ('in_progress', 'printing', 'paused')
|
|
ORDER BY id DESC
|
|
LIMIT 1
|
|
""",
|
|
(row["printer_host"], fname),
|
|
).fetchone()
|
|
if not hist:
|
|
skipped += 1
|
|
continue
|
|
store.adopt_live_run(conn, row["printer_host"], fname, hist["id"])
|
|
still = conn.execute(
|
|
"SELECT 1 FROM runs WHERE id = %s", (row["id"],)
|
|
).fetchone()
|
|
if still:
|
|
skipped += 1
|
|
else:
|
|
cleaned += 1
|
|
conn.commit()
|
|
print(
|
|
"verbunden=%d orphan_adopted=%d orphan_skipped=%d"
|
|
% (merged, cleaned, skipped)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|