fix: Threading-Lock verhindert parallele Scans, Status-API, Button-Feedback
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ba0893a337
commit
639e6a4fc3
2 changed files with 103 additions and 36 deletions
|
|
@ -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():
|
||||||
|
|
|
||||||
|
|
@ -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 ────────────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue