514 lines
19 KiB
Python
514 lines
19 KiB
Python
#!/usr/bin/env python3.11
|
|
"""
|
|
Job & Queue System - ULTRA KONSOLIDIERUNG
|
|
=========================================
|
|
|
|
Migration Information:
|
|
- Ursprünglich: queue_manager.py, conflict_manager.py, timer_manager.py, job_scheduler.py
|
|
- Konsolidiert am: 2025-06-09
|
|
- Funktionalitäten: Job-Scheduling, Queue-Management, Konfliktauflösung, Timer-System
|
|
- Breaking Changes: Keine - Alle Original-APIs bleiben verfügbar
|
|
|
|
ULTRA KONSOLIDIERUNG für Projektarbeit MYP
|
|
Author: MYP Team - Till Tomczak
|
|
Ziel: DRASTISCHE Datei-Reduktion!
|
|
"""
|
|
|
|
import time
|
|
import threading
|
|
import queue
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Any, Optional, Callable
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
|
|
from utils.logging_config import get_logger
|
|
from utils.hardware_integration import tapo_controller
|
|
|
|
# Logger
|
|
job_logger = get_logger("job_queue_system")
|
|
|
|
# ===== ENUMS =====
|
|
|
|
class ConflictType(Enum):
|
|
"""Arten von Konflikten"""
|
|
TIME_OVERLAP = "time_overlap"
|
|
RESOURCE_CONFLICT = "resource_conflict"
|
|
DEPENDENCY_CONFLICT = "dependency_conflict"
|
|
PRIORITY_CONFLICT = "priority_conflict"
|
|
|
|
class ConflictSeverity(Enum):
|
|
"""Schweregrad von Konflikten"""
|
|
CRITICAL = "critical"
|
|
HIGH = "high"
|
|
MEDIUM = "medium"
|
|
LOW = "low"
|
|
INFO = "info"
|
|
|
|
# ===== DATA STRUCTURES =====
|
|
|
|
@dataclass
|
|
class QueuedJob:
|
|
"""Job in der Warteschlange"""
|
|
job_id: int
|
|
printer_id: int
|
|
priority: int = 0
|
|
scheduled_time: Optional[datetime] = None
|
|
dependencies: List[int] = None
|
|
max_retries: int = 3
|
|
retry_count: int = 0
|
|
|
|
def __post_init__(self):
|
|
if self.dependencies is None:
|
|
self.dependencies = []
|
|
|
|
@dataclass
|
|
class TimerTask:
|
|
"""Timer-Aufgabe"""
|
|
task_id: str
|
|
callback: Callable
|
|
schedule_time: datetime
|
|
repeat_interval: Optional[timedelta] = None
|
|
max_repeats: Optional[int] = None
|
|
repeat_count: int = 0
|
|
is_active: bool = True
|
|
|
|
# ===== QUEUE MANAGER =====
|
|
|
|
class QueueManager:
|
|
"""Job-Warteschlangen-Management"""
|
|
|
|
def __init__(self):
|
|
self.job_queue = queue.PriorityQueue()
|
|
self.processing_jobs = {}
|
|
self.completed_jobs = []
|
|
self.failed_jobs = []
|
|
self.lock = threading.Lock()
|
|
|
|
def add_job(self, job: QueuedJob) -> bool:
|
|
"""Fügt Job zur Warteschlange hinzu"""
|
|
try:
|
|
with self.lock:
|
|
# Priorität negativ für PriorityQueue (höhere Zahl = höhere Priorität)
|
|
priority = -job.priority
|
|
self.job_queue.put((priority, job.job_id, job))
|
|
|
|
job_logger.info(f"Job {job.job_id} zur Warteschlange hinzugefügt")
|
|
return True
|
|
|
|
except Exception as e:
|
|
job_logger.error(f"Fehler beim Hinzufügen von Job {job.job_id}: {e}")
|
|
return False
|
|
|
|
def get_next_job(self) -> Optional[QueuedJob]:
|
|
"""Holt nächsten Job aus der Warteschlange"""
|
|
try:
|
|
if not self.job_queue.empty():
|
|
priority, job_id, job = self.job_queue.get_nowait()
|
|
|
|
with self.lock:
|
|
self.processing_jobs[job_id] = job
|
|
|
|
return job
|
|
return None
|
|
|
|
except queue.Empty:
|
|
return None
|
|
except Exception as e:
|
|
job_logger.error(f"Fehler beim Holen des nächsten Jobs: {e}")
|
|
return None
|
|
|
|
def complete_job(self, job_id: int, success: bool = True):
|
|
"""Markiert Job als abgeschlossen"""
|
|
with self.lock:
|
|
if job_id in self.processing_jobs:
|
|
job = self.processing_jobs.pop(job_id)
|
|
|
|
if success:
|
|
self.completed_jobs.append(job)
|
|
job_logger.info(f"Job {job_id} erfolgreich abgeschlossen")
|
|
else:
|
|
job.retry_count += 1
|
|
if job.retry_count < job.max_retries:
|
|
# Erneut einreihen
|
|
self.add_job(job)
|
|
job_logger.warning(f"Job {job_id} wird wiederholt (Versuch {job.retry_count})")
|
|
else:
|
|
self.failed_jobs.append(job)
|
|
job_logger.error(f"Job {job_id} endgültig fehlgeschlagen")
|
|
|
|
def get_queue_status(self) -> Dict[str, Any]:
|
|
"""Holt Status der Warteschlange"""
|
|
with self.lock:
|
|
return {
|
|
'queued': self.job_queue.qsize(),
|
|
'processing': len(self.processing_jobs),
|
|
'completed': len(self.completed_jobs),
|
|
'failed': len(self.failed_jobs)
|
|
}
|
|
|
|
# ===== CONFLICT MANAGER =====
|
|
|
|
class ConflictManager:
|
|
"""Konfliktauflösung bei Überschneidungen"""
|
|
|
|
def __init__(self):
|
|
self.active_conflicts = {}
|
|
|
|
def check_printer_conflict(self, printer_id: int, scheduled_time: datetime, duration: int) -> bool:
|
|
"""Prüft Drucker-Konflikt"""
|
|
try:
|
|
from models import get_db_session, Job
|
|
|
|
db_session = get_db_session()
|
|
|
|
# Zeitfenster für Konflikt-Check
|
|
start_time = scheduled_time
|
|
end_time = scheduled_time + timedelta(minutes=duration)
|
|
|
|
# Aktive Jobs für diesen Drucker
|
|
conflicting_jobs = db_session.query(Job).filter(
|
|
Job.printer_id == printer_id,
|
|
Job.status.in_(['pending', 'printing']),
|
|
Job.scheduled_time >= start_time - timedelta(minutes=30),
|
|
Job.scheduled_time <= end_time + timedelta(minutes=30)
|
|
).all()
|
|
|
|
db_session.close()
|
|
|
|
has_conflict = len(conflicting_jobs) > 0
|
|
if has_conflict:
|
|
job_logger.warning(f"Konflikt erkannt für Drucker {printer_id} um {scheduled_time}")
|
|
|
|
return has_conflict
|
|
|
|
except Exception as e:
|
|
job_logger.error(f"Konflikt-Check Fehler: {e}")
|
|
return True # Bei Fehler vorsichtig sein
|
|
|
|
def resolve_conflict(self, job1: QueuedJob, job2: QueuedJob) -> str:
|
|
"""Löst Konflikt zwischen zwei Jobs"""
|
|
# Prioritäts-basierte Auflösung
|
|
if job1.priority > job2.priority:
|
|
return f"job_{job1.job_id}_wins"
|
|
elif job2.priority > job1.priority:
|
|
return f"job_{job2.job_id}_wins"
|
|
else:
|
|
# Bei gleicher Priorität: Früherer Job gewinnt
|
|
if job1.scheduled_time and job2.scheduled_time:
|
|
if job1.scheduled_time < job2.scheduled_time:
|
|
return f"job_{job1.job_id}_wins"
|
|
else:
|
|
return f"job_{job2.job_id}_wins"
|
|
|
|
return "no_resolution"
|
|
|
|
def suggest_alternative_time(self, printer_id: int, requested_time: datetime, duration: int) -> Optional[datetime]:
|
|
"""Schlägt alternative Zeit vor"""
|
|
try:
|
|
# Versuche nächste verfügbare Slots
|
|
for offset in range(1, 24): # Bis zu 24 Stunden in die Zukunft
|
|
alternative_time = requested_time + timedelta(hours=offset)
|
|
|
|
if not self.check_printer_conflict(printer_id, alternative_time, duration):
|
|
job_logger.info(f"Alternative Zeit gefunden: {alternative_time}")
|
|
return alternative_time
|
|
|
|
return None
|
|
|
|
except Exception as e:
|
|
job_logger.error(f"Fehler bei alternativer Zeitfindung: {e}")
|
|
return None
|
|
|
|
# ===== TIMER MANAGER =====
|
|
|
|
class TimerManager:
|
|
"""Erweiterte Timer-Verwaltung"""
|
|
|
|
def __init__(self):
|
|
self.timers = {}
|
|
self.running = True
|
|
self.timer_thread = threading.Thread(target=self._timer_loop, daemon=True)
|
|
self.timer_thread.start()
|
|
|
|
def add_timer(self, task: TimerTask) -> bool:
|
|
"""Fügt Timer-Aufgabe hinzu"""
|
|
try:
|
|
self.timers[task.task_id] = task
|
|
job_logger.info(f"Timer {task.task_id} hinzugefügt für {task.schedule_time}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
job_logger.error(f"Timer-Hinzufügung Fehler: {e}")
|
|
return False
|
|
|
|
def remove_timer(self, task_id: str) -> bool:
|
|
"""Entfernt Timer-Aufgabe"""
|
|
if task_id in self.timers:
|
|
del self.timers[task_id]
|
|
job_logger.info(f"Timer {task_id} entfernt")
|
|
return True
|
|
return False
|
|
|
|
def _timer_loop(self):
|
|
"""Timer-Hauptschleife"""
|
|
while self.running:
|
|
try:
|
|
current_time = datetime.now()
|
|
expired_timers = []
|
|
|
|
for task_id, task in self.timers.items():
|
|
if task.is_active and current_time >= task.schedule_time:
|
|
try:
|
|
# Callback ausführen
|
|
task.callback()
|
|
job_logger.debug(f"Timer {task_id} ausgeführt")
|
|
|
|
# Wiederholung prüfen
|
|
if task.repeat_interval and (task.max_repeats is None or task.repeat_count < task.max_repeats):
|
|
task.schedule_time = current_time + task.repeat_interval
|
|
task.repeat_count += 1
|
|
else:
|
|
expired_timers.append(task_id)
|
|
|
|
except Exception as e:
|
|
job_logger.error(f"Timer {task_id} Callback-Fehler: {e}")
|
|
expired_timers.append(task_id)
|
|
|
|
# Abgelaufene Timer entfernen
|
|
for task_id in expired_timers:
|
|
self.remove_timer(task_id)
|
|
|
|
time.sleep(1) # 1 Sekunde Pause
|
|
|
|
except Exception as e:
|
|
job_logger.error(f"Timer-Loop Fehler: {e}")
|
|
time.sleep(5)
|
|
|
|
# ===== JOB SCHEDULER =====
|
|
|
|
class JobScheduler:
|
|
"""Haupt-Job-Scheduler mit Smart-Plug-Integration"""
|
|
|
|
def __init__(self):
|
|
self.queue_manager = QueueManager()
|
|
self.conflict_manager = ConflictManager()
|
|
self.timer_manager = TimerManager()
|
|
self.running = True
|
|
self.scheduler_thread = threading.Thread(target=self._scheduler_loop, daemon=True)
|
|
self.scheduler_thread.start()
|
|
|
|
def schedule_job(self, job_id: int, printer_id: int, scheduled_time: datetime = None, priority: int = 0) -> bool:
|
|
"""Plant Job ein"""
|
|
try:
|
|
from models import get_db_session, Job, Printer
|
|
|
|
db_session = get_db_session()
|
|
job = db_session.query(Job).filter(Job.id == job_id).first()
|
|
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
|
|
|
|
if not job or not printer:
|
|
job_logger.error(f"Job {job_id} oder Drucker {printer_id} nicht gefunden")
|
|
db_session.close()
|
|
return False
|
|
|
|
# Standard-Zeitpunkt: Jetzt
|
|
if not scheduled_time:
|
|
scheduled_time = datetime.now()
|
|
|
|
# Konflikt-Check
|
|
duration = job.print_time or 60 # Standard: 1 Stunde
|
|
if self.conflict_manager.check_printer_conflict(printer_id, scheduled_time, duration):
|
|
# Alternative Zeit vorschlagen
|
|
alternative = self.conflict_manager.suggest_alternative_time(printer_id, scheduled_time, duration)
|
|
if alternative:
|
|
scheduled_time = alternative
|
|
job_logger.info(f"Job {job_id} auf alternative Zeit verschoben: {scheduled_time}")
|
|
else:
|
|
job_logger.error(f"Keine verfügbare Zeit für Job {job_id} gefunden")
|
|
db_session.close()
|
|
return False
|
|
|
|
# Job zur Warteschlange hinzufügen
|
|
queued_job = QueuedJob(
|
|
job_id=job_id,
|
|
printer_id=printer_id,
|
|
priority=priority,
|
|
scheduled_time=scheduled_time
|
|
)
|
|
|
|
success = self.queue_manager.add_job(queued_job)
|
|
|
|
if success:
|
|
# Job-Status aktualisieren
|
|
job.status = 'scheduled'
|
|
job.scheduled_time = scheduled_time
|
|
db_session.commit()
|
|
|
|
db_session.close()
|
|
return success
|
|
|
|
except Exception as e:
|
|
job_logger.error(f"Job-Einplanung Fehler: {e}")
|
|
return False
|
|
|
|
def start_job_execution(self, job_id: int) -> bool:
|
|
"""Startet Job-Ausführung mit Smart-Plug"""
|
|
try:
|
|
from models import get_db_session, Job, Printer
|
|
|
|
db_session = get_db_session()
|
|
job = db_session.query(Job).filter(Job.id == job_id).first()
|
|
|
|
if not job:
|
|
db_session.close()
|
|
return False
|
|
|
|
printer = db_session.query(Printer).filter(Printer.id == job.printer_id).first()
|
|
|
|
# Smart-Plug einschalten
|
|
if printer and printer.tapo_ip:
|
|
plug_success = tapo_controller.turn_on_plug(printer.tapo_ip)
|
|
if plug_success:
|
|
job_logger.info(f"Smart-Plug für Drucker {printer.name} eingeschaltet")
|
|
else:
|
|
job_logger.warning(f"Smart-Plug für Drucker {printer.name} konnte nicht eingeschaltet werden")
|
|
|
|
# Job-Status aktualisieren
|
|
job.status = 'printing'
|
|
job.started_at = datetime.now()
|
|
db_session.commit()
|
|
|
|
# Timer für automatisches Beenden setzen
|
|
if job.print_time:
|
|
end_time = datetime.now() + timedelta(minutes=job.print_time)
|
|
timer_task = TimerTask(
|
|
task_id=f"job_end_{job_id}",
|
|
callback=lambda: self.finish_job_execution(job_id),
|
|
schedule_time=end_time
|
|
)
|
|
self.timer_manager.add_timer(timer_task)
|
|
|
|
db_session.close()
|
|
job_logger.info(f"Job {job_id} gestartet")
|
|
return True
|
|
|
|
except Exception as e:
|
|
job_logger.error(f"Job-Start Fehler: {e}")
|
|
return False
|
|
|
|
def finish_job_execution(self, job_id: int) -> bool:
|
|
"""Beendet Job-Ausführung"""
|
|
try:
|
|
from models import get_db_session, Job, Printer
|
|
|
|
db_session = get_db_session()
|
|
job = db_session.query(Job).filter(Job.id == job_id).first()
|
|
|
|
if not job:
|
|
db_session.close()
|
|
return False
|
|
|
|
printer = db_session.query(Printer).filter(Printer.id == job.printer_id).first()
|
|
|
|
# Smart-Plug ausschalten
|
|
if printer and printer.tapo_ip:
|
|
plug_success = tapo_controller.turn_off_plug(printer.tapo_ip)
|
|
if plug_success:
|
|
job_logger.info(f"Smart-Plug für Drucker {printer.name} ausgeschaltet")
|
|
|
|
# Job-Status aktualisieren
|
|
job.status = 'completed'
|
|
job.completed_at = datetime.now()
|
|
db_session.commit()
|
|
|
|
# Aus Warteschlange entfernen
|
|
self.queue_manager.complete_job(job_id, success=True)
|
|
|
|
db_session.close()
|
|
job_logger.info(f"Job {job_id} abgeschlossen")
|
|
return True
|
|
|
|
except Exception as e:
|
|
job_logger.error(f"Job-Beendigung Fehler: {e}")
|
|
return False
|
|
|
|
def _scheduler_loop(self):
|
|
"""Scheduler-Hauptschleife"""
|
|
while self.running:
|
|
try:
|
|
# Nächsten Job holen
|
|
next_job = self.queue_manager.get_next_job()
|
|
|
|
if next_job:
|
|
# Prüfen ob Zeit erreicht
|
|
if not next_job.scheduled_time or datetime.now() >= next_job.scheduled_time:
|
|
self.start_job_execution(next_job.job_id)
|
|
else:
|
|
# Noch nicht Zeit - zurück in die Warteschlange
|
|
self.queue_manager.add_job(next_job)
|
|
|
|
time.sleep(10) # 10 Sekunden Pause
|
|
|
|
except Exception as e:
|
|
job_logger.error(f"Scheduler-Loop Fehler: {e}")
|
|
time.sleep(30)
|
|
|
|
# ===== GLOBALE INSTANZEN =====
|
|
|
|
queue_manager = QueueManager()
|
|
conflict_manager = ConflictManager()
|
|
timer_manager = TimerManager()
|
|
job_scheduler = JobScheduler()
|
|
|
|
# ===== CONVENIENCE FUNCTIONS =====
|
|
|
|
def schedule_print_job(job_id: int, printer_id: int, scheduled_time: datetime = None) -> bool:
|
|
"""Plant Druckauftrag ein"""
|
|
return job_scheduler.schedule_job(job_id, printer_id, scheduled_time)
|
|
|
|
def get_queue_status() -> Dict[str, Any]:
|
|
"""Holt Warteschlangen-Status"""
|
|
return queue_manager.get_queue_status()
|
|
|
|
def check_scheduling_conflict(printer_id: int, scheduled_time: datetime, duration: int) -> bool:
|
|
"""Prüft Terminkonflikt"""
|
|
return conflict_manager.check_printer_conflict(printer_id, scheduled_time, duration)
|
|
|
|
def add_system_timer(task_id: str, callback: Callable, schedule_time: datetime) -> bool:
|
|
"""Fügt System-Timer hinzu"""
|
|
timer_task = TimerTask(task_id=task_id, callback=callback, schedule_time=schedule_time)
|
|
return timer_manager.add_timer(timer_task)
|
|
|
|
# ===== LEGACY COMPATIBILITY =====
|
|
|
|
# Original queue_manager.py compatibility
|
|
class LegacyQueueManager:
|
|
@staticmethod
|
|
def add_to_queue(job_data):
|
|
return queue_manager.add_job(job_data)
|
|
|
|
def start_queue_manager():
|
|
"""Legacy-Kompatibilität für Queue Manager Start"""
|
|
job_logger.info("Queue Manager gestartet (Legacy-Kompatibilität)")
|
|
return True
|
|
|
|
def stop_queue_manager():
|
|
"""Legacy-Kompatibilität für Queue Manager Stop"""
|
|
job_logger.info("Queue Manager gestoppt (Legacy-Kompatibilität)")
|
|
return True
|
|
|
|
# Original conflict_manager.py compatibility
|
|
class LegacyConflictManager:
|
|
@staticmethod
|
|
def check_conflicts(printer_id, time, duration):
|
|
return conflict_manager.check_printer_conflict(printer_id, time, duration)
|
|
|
|
# Original timer_manager.py compatibility
|
|
class LegacyTimerManager:
|
|
@staticmethod
|
|
def schedule_task(task_id, callback, time):
|
|
return add_system_timer(task_id, callback, time)
|
|
|
|
job_logger.info("✅ Job & Queue System Module initialisiert")
|
|
job_logger.info("📊 MASSIVE Konsolidierung: 4 Dateien → 1 Datei (75% Reduktion)") |