fix: Threading-Lock verhindert parallele Scans, Status-API, Button-Feedback

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Cursor 2026-02-25 16:50:01 +07:00
parent ba0893a337
commit 639e6a4fc3
2 changed files with 103 additions and 36 deletions

View file

@ -7,6 +7,10 @@ from datetime import datetime, timedelta
from db import init_db, get_conn, log from db import init_db, get_conn, log
from ki import auswerten from ki import auswerten
# Verhindert dass zwei Läufe gleichzeitig laufen
_scan_lock = threading.Lock()
_lauf_aktiv = False
def get_nodes(): def get_nodes():
conn = get_conn() conn = get_conn()
@ -105,8 +109,18 @@ def speichere_preise(results, node_name, job):
def scraping_lauf(label="Standard", flex_tage_liste=None): def scraping_lauf(label="Standard", flex_tage_liste=None):
""" """
Führt alle aktiven Jobs auf allen Nodes aus. Führt alle aktiven Jobs auf allen Nodes aus.
flex_tage_liste: Liste von 'tage'-Werten für Datums-Flexibilität (z.B. [27,28,30,32,33]) Übersprungen wenn ein anderer Lauf noch aktiv ist (Lock-Schutz).
""" """
global _lauf_aktiv
if not _scan_lock.acquire(blocking=False):
log(f"Lauf [{label}] übersprungen — vorheriger Lauf noch aktiv", "WARN")
return
_lauf_aktiv = True
start = datetime.now()
try:
log(f"=== Scraping-Lauf [{label}] gestartet ===") log(f"=== Scraping-Lauf [{label}] gestartet ===")
nodes = get_nodes() nodes = get_nodes()
jobs = get_aktive_jobs() jobs = get_aktive_jobs()
@ -115,45 +129,68 @@ def scraping_lauf(label="Standard", flex_tage_liste=None):
log("Keine aktiven Nodes konfiguriert", "WARN") log("Keine aktiven Nodes konfiguriert", "WARN")
return return
tage_varianten = flex_tage_liste or [None] # None = Job-Default verwenden tage_varianten = flex_tage_liste or [None]
online = 0 online = 0
fehler = 0
preise_gesamt = 0
for node in nodes: for node in nodes:
if node_ping(node): if node_ping(node):
update_node_status(node["name"], "online") update_node_status(node["name"], "online")
online += 1 online += 1
for job in jobs: for job in jobs:
for tage_var in tage_varianten: for tage_var in tage_varianten:
dispatch_job(node, job, tage_override=tage_var) try:
ok = dispatch_job(node, job, tage_override=tage_var)
if not ok:
fehler += 1
except Exception as e:
log(f"Job-Fehler {node['name']}/{job['scanner']}: {e}", "ERROR")
fehler += 1
else: else:
log(f"Node {node['name']} nicht erreichbar", "WARN") log(f"Node {node['name']} nicht erreichbar", "WARN")
update_node_status(node["name"], "offline") update_node_status(node["name"], "offline")
log(f"Scraping [{label}] abgeschlossen — {online}/{len(nodes)} Nodes online") dauer = round((datetime.now() - start).total_seconds())
log(f"Scraping [{label}] fertig — {online}/{len(nodes)} Nodes | "
f"{fehler} Fehler | {dauer}s Laufzeit")
try:
auswerten() auswerten()
except Exception as e:
log(f"KI-Fehler: {e}", "ERROR")
log(f"=== Lauf [{label}] beendet ===") log(f"=== Lauf [{label}] beendet ===")
finally:
_lauf_aktiv = False
_scan_lock.release()
def standard_lauf(): def standard_lauf():
"""Normaler 4×-täglich Lauf mit Standard-Datum.""" """30-Minuten-Takt — in eigenem Thread damit der Scheduler nicht blockiert."""
scraping_lauf(label=datetime.now().strftime("%H:%M")) threading.Thread(
target=scraping_lauf,
kwargs={"label": datetime.now().strftime("%H:%M")},
daemon=True
).start()
def flex_lauf(): def flex_lauf():
""" """Di/Mi 23:30 — ±3 Tage Datumsfenster."""
Di/Mi Nacht: ±3 Tage Datums-Flexibilität. wochentag = datetime.now().weekday()
Sucht Abflug in 27-33 Tagen statt nur 30 Tagen.
"""
wochentag = datetime.now().weekday() # 0=Mo, 1=Di, 2=Mi
if wochentag not in (1, 2): if wochentag not in (1, 2):
log("Flex-Lauf: heute kein Di/Mi — übersprungen") log("Flex-Lauf: heute kein Di/Mi — übersprungen")
return return
# 7 Varianten: 30±3 Tage
basis = 30 basis = 30
flex_varianten = list(range(basis - 3, basis + 4)) # [27,28,29,30,31,32,33] flex_varianten = list(range(basis - 3, basis + 4))
log(f"=== Flex-Lauf Di/Mi ±3 Tage: {flex_varianten} ===") log(f"=== Flex-Lauf Di/Mi ±3 Tage: {flex_varianten} ===")
scraping_lauf(label="Flex-Di/Mi", flex_tage_liste=flex_varianten) threading.Thread(
target=scraping_lauf,
kwargs={"label": "Flex-Di/Mi", "flex_tage_liste": flex_varianten},
daemon=True
).start()
def run(): def run():

View file

@ -114,7 +114,7 @@ OVERVIEW_HTML = BASE_HTML.replace("{% block content %}{% endblock %}", """
<div style="margin-top:0.5rem;font-size:0.75rem;color:#475569" id="ki-datum"></div> <div style="margin-top:0.5rem;font-size:0.75rem;color:#475569" id="ki-datum"></div>
</div> </div>
<div style="margin-top:1rem"> <div style="margin-top:1rem">
<button class="btn btn-green" onclick="manuellScrapen()"> Jetzt scrapen</button> <button id="scan-btn" class="btn btn-green" onclick="manuellScrapen()"> Jetzt scrapen</button>
</div> </div>
</div> </div>
</div> </div>
@ -289,12 +289,33 @@ async function ladeUebersicht() {
}); });
} }
async function pruefeScanStatus() {
const d = await fetch('/api/scrape/status').then(r=>r.json());
const btn = document.getElementById('scan-btn');
if (d.running) {
btn.textContent = '⏳ Läuft...';
btn.disabled = true;
btn.style.background = '#92400e';
setTimeout(pruefeScanStatus, 5000);
} else {
btn.textContent = '▶ Jetzt scrapen';
btn.disabled = false;
btn.style.background = '';
}
}
async function manuellScrapen() { async function manuellScrapen() {
if (!confirm('Scraping jetzt starten?')) return;
const r = await fetch('/api/scrape/now', {method:'POST'}); const r = await fetch('/api/scrape/now', {method:'POST'});
const d = await r.json(); const d = await r.json();
if (r.status === 409) {
alert(d.message); alert(d.message);
return;
} }
pruefeScanStatus();
}
pruefeScanStatus();
setInterval(pruefeScanStatus, 10000);
ladeUebersicht(); ladeUebersicht();
</script> </script>
@ -491,12 +512,21 @@ def api_schedule():
@app.route("/api/scrape/now", methods=["POST"]) @app.route("/api/scrape/now", methods=["POST"])
def api_scrape_now(): def api_scrape_now():
from scheduler import _lauf_aktiv
if _lauf_aktiv:
return jsonify({"message": "⚠ Lauf bereits aktiv — bitte warten", "running": True}), 409
threading.Thread( threading.Thread(
target=scraping_lauf, target=scraping_lauf,
kwargs={"label": "Manuell"}, kwargs={"label": "Manuell"},
daemon=True daemon=True
).start() ).start()
return jsonify({"message": "Scraping gestartet — läuft im Hintergrund"}) return jsonify({"message": "Scraping gestartet — läuft im Hintergrund", "running": False})
@app.route("/api/scrape/status")
def api_scrape_status():
from scheduler import _lauf_aktiv
return jsonify({"running": _lauf_aktiv})
# ─── Seiten ──────────────────────────────────────────────────────────────────── # ─── Seiten ────────────────────────────────────────────────────────────────────