Projektarbeit-MYP/backend/app/utils/queue_manager.py

309 lines
12 KiB
Python

"""
Queue Manager für die Verwaltung von Druckjobs in Warteschlangen.
Überwacht offline Drucker und aktiviert Jobs automatisch.
"""
import threading
import time
import logging
import subprocess
import os
import requests
from datetime import datetime, timedelta
from typing import List, Dict, Optional, Tuple
from contextlib import contextmanager
from models import get_db_session, Job, Printer, User, Notification
from utils.logging_config import get_logger
# Logger für Queue-Manager
queue_logger = get_logger("queue_manager")
def check_printer_status(ip_address: str, timeout: int = 5) -> Tuple[str, bool]:
"""
Vereinfachte Drucker-Status-Prüfung für den Queue Manager.
Args:
ip_address: IP-Adresse der Drucker-Steckdose
timeout: Timeout in Sekunden (Standard: 5)
Returns:
Tuple[str, bool]: (Status, Aktiv) - Status ist "online" oder "offline", Aktiv ist True/False
"""
if not ip_address or ip_address.strip() == "":
return "offline", False
try:
# Ping-Test um Erreichbarkeit zu prüfen
if os.name == 'nt': # Windows
cmd = ['ping', '-n', '1', '-w', str(timeout * 1000), ip_address.strip()]
else: # Unix/Linux/macOS
cmd = ['ping', '-c', '1', '-W', str(timeout), ip_address.strip()]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout + 1
)
# Wenn Ping erfolgreich ist, als online betrachten
if result.returncode == 0:
queue_logger.debug(f"✅ Drucker {ip_address} ist erreichbar (Ping erfolgreich)")
return "online", True
else:
queue_logger.debug(f"❌ Drucker {ip_address} nicht erreichbar (Ping fehlgeschlagen)")
return "offline", False
except subprocess.TimeoutExpired:
queue_logger.warning(f"⏱️ Ping-Timeout für Drucker {ip_address} nach {timeout} Sekunden")
return "offline", False
except Exception as e:
queue_logger.error(f"❌ Fehler beim Status-Check für Drucker {ip_address}: {str(e)}")
return "offline", False
class PrinterQueueManager:
"""
Verwaltet die Warteschlangen für offline Drucker und überwacht deren Status.
"""
def __init__(self):
self.is_running = False
self.monitor_thread = None
self.check_interval = 120 # 2 Minuten zwischen Status-Checks
self.last_status_cache = {} # Cache für letzten bekannten Status
self.notification_cooldown = {} # Verhindert Spam-Benachrichtigungen
def start(self):
"""Startet den Queue-Manager."""
if not self.is_running:
self.is_running = True
self.monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True)
self.monitor_thread.start()
queue_logger.info("✅ Printer Queue Manager erfolgreich gestartet")
def stop(self):
"""Stoppt den Queue-Manager."""
self.is_running = False
if self.monitor_thread and self.monitor_thread.is_alive():
self.monitor_thread.join(timeout=5)
queue_logger.info("❌ Printer Queue Manager gestoppt")
def _monitor_loop(self):
"""Hauptschleife für die Überwachung der Drucker."""
queue_logger.info(f"🔄 Queue-Überwachung gestartet (Intervall: {self.check_interval} Sekunden)")
while self.is_running:
try:
self._check_waiting_jobs()
time.sleep(self.check_interval)
except Exception as e:
queue_logger.error(f"❌ Fehler in Monitor-Schleife: {str(e)}")
time.sleep(30) # Kürzere Wartezeit bei Fehlern
def _check_waiting_jobs(self):
"""Überprüft alle wartenden Jobs und aktiviert sie bei verfügbaren Druckern."""
db_session = get_db_session()
try:
# Alle wartenden Jobs abrufen
waiting_jobs = db_session.query(Job).filter(
Job.status == "waiting_for_printer"
).all()
if not waiting_jobs:
return
queue_logger.info(f"🔍 Überprüfe {len(waiting_jobs)} wartende Jobs...")
activated_jobs = []
for job in waiting_jobs:
# Drucker-Status prüfen
printer = db_session.query(Printer).get(job.printer_id)
if not printer:
continue
# Status-Check mit Cache-Optimierung
printer_key = f"printer_{printer.id}"
current_status = None
try:
if printer.plug_ip:
status, active = check_printer_status(printer.plug_ip, timeout=5)
current_status = "online" if (status == "online" and active) else "offline"
else:
current_status = "offline"
except Exception as e:
queue_logger.warning(f"⚠️ Status-Check für Drucker {printer.name} fehlgeschlagen: {str(e)}")
current_status = "offline"
# Prüfen, ob Drucker online geworden ist
last_status = self.last_status_cache.get(printer_key, "offline")
self.last_status_cache[printer_key] = current_status
if current_status == "online" and last_status == "offline":
# Drucker ist online geworden!
queue_logger.info(f"🟢 Drucker {printer.name} ist ONLINE geworden - aktiviere wartende Jobs")
# Job aktivieren
job.status = "scheduled"
printer.status = "available"
printer.active = True
printer.last_checked = datetime.now()
activated_jobs.append({
"job": job,
"printer": printer
})
elif current_status == "online":
# Drucker ist bereits online, Job kann aktiviert werden
job.status = "scheduled"
printer.status = "available"
printer.active = True
printer.last_checked = datetime.now()
activated_jobs.append({
"job": job,
"printer": printer
})
else:
# Drucker bleibt offline
printer.status = "offline"
printer.active = False
printer.last_checked = datetime.now()
# Speichere alle Änderungen
if activated_jobs:
db_session.commit()
queue_logger.info(f"{len(activated_jobs)} Jobs erfolgreich aktiviert")
# Benachrichtigungen versenden
for item in activated_jobs:
self._send_job_activation_notification(item["job"], item["printer"])
else:
# Auch offline-Status speichern
db_session.commit()
except Exception as e:
db_session.rollback()
queue_logger.error(f"❌ Fehler beim Überprüfen wartender Jobs: {str(e)}")
finally:
db_session.close()
def _send_job_activation_notification(self, job: Job, printer: Printer):
"""Sendet eine Benachrichtigung, wenn ein Job aktiviert wird."""
try:
# Cooldown prüfen (keine Spam-Benachrichtigungen)
cooldown_key = f"job_{job.id}_activated"
now = datetime.now()
if cooldown_key in self.notification_cooldown:
last_notification = self.notification_cooldown[cooldown_key]
if (now - last_notification).total_seconds() < 300: # 5 Minuten Cooldown
return
self.notification_cooldown[cooldown_key] = now
# Benachrichtigung erstellen
db_session = get_db_session()
try:
user = db_session.query(User).get(job.user_id)
if not user:
return
notification = Notification(
user_id=user.id,
type="job_activated",
payload={
"job_id": job.id,
"job_name": job.name,
"printer_id": printer.id,
"printer_name": printer.name,
"start_time": job.start_at.isoformat() if job.start_at else None,
"message": f"🎉 Gute Nachrichten! Drucker '{printer.name}' ist online. Ihr Job '{job.name}' wurde aktiviert und startet bald."
}
)
db_session.add(notification)
db_session.commit()
queue_logger.info(f"📧 Benachrichtigung für User {user.name} gesendet: Job {job.name} aktiviert")
except Exception as e:
db_session.rollback()
queue_logger.error(f"❌ Fehler beim Erstellen der Benachrichtigung: {str(e)}")
finally:
db_session.close()
except Exception as e:
queue_logger.error(f"❌ Fehler beim Senden der Aktivierungs-Benachrichtigung: {str(e)}")
def get_queue_status(self) -> Dict:
"""Gibt den aktuellen Status der Warteschlangen zurück."""
db_session = get_db_session()
try:
# Wartende Jobs zählen
waiting_jobs = db_session.query(Job).filter(
Job.status == "waiting_for_printer"
).count()
# Offline Drucker mit wartenden Jobs
offline_printers_with_queue = db_session.query(Printer).join(Job).filter(
Printer.status == "offline",
Job.status == "waiting_for_printer"
).distinct().count()
# Online Drucker
online_printers = db_session.query(Printer).filter(
Printer.status == "available"
).count()
total_printers = db_session.query(Printer).count()
return {
"waiting_jobs": waiting_jobs,
"offline_printers_with_queue": offline_printers_with_queue,
"online_printers": online_printers,
"total_printers": total_printers,
"queue_manager_running": self.is_running,
"last_check": datetime.now().isoformat(),
"check_interval_seconds": self.check_interval
}
except Exception as e:
queue_logger.error(f"❌ Fehler beim Abrufen des Queue-Status: {str(e)}")
return {
"error": str(e),
"queue_manager_running": self.is_running
}
finally:
db_session.close()
# Globale Instanz des Queue-Managers
_queue_manager_instance = None
def get_queue_manager() -> PrinterQueueManager:
"""Gibt die globale Instanz des Queue-Managers zurück."""
global _queue_manager_instance
if _queue_manager_instance is None:
_queue_manager_instance = PrinterQueueManager()
return _queue_manager_instance
def start_queue_manager():
"""Startet den globalen Queue-Manager."""
manager = get_queue_manager()
manager.start()
return manager
def stop_queue_manager():
"""Stoppt den globalen Queue-Manager."""
global _queue_manager_instance
if _queue_manager_instance:
_queue_manager_instance.stop()
_queue_manager_instance = None